[USACO]低价购买

Posted by Panda2134's Blog on July 31, 2017

链接:Luogu-P1108

这个题刚看到的时候我是懵逼的……着实被题解惊艳到了,完全想不到还有这么优美的解法。 这些解法来自各位dalao的题解,整理如下。

目录

审题

求最长下降子序列长度以及方案数。

解法1

这是我认为最复杂也最精妙的一种方法:二分单调栈。对于这种方案计数的问题,也可以做到$O(n \log n)$ 的复杂度。

修改状态

如果我们拘泥于普通 LIS 类问题的状态定义的话,显然很难以较低复杂度解决这个问题;仿照普通LIS的树状数组$O(n \log n)$做法,考虑把每个数字的 加入状态之中。

令 $f(i)$ 为以数字 i (注意:不是位置)结尾的最长下降子序列长度。于是显然有状态转移方程:$f(i)=\max \{f(j) \vert j>i\}+1$ 。注意这个方程中的数字 $j$ 的下标一定在 $i$ 的左边。

进行优化

我们先来考虑下如何优化利用这个方程求 LDS 的过程。暂时不考虑方案计数。

朴素的方法是在原序列中从左往右更新,对于一个位置 $i$ ,考虑它左边的所有满足 $W_j > W_i$ 的数字 $W_j$,用 $f(W_j)$ 去更新 $f(W_i)$ 。复杂度和原来的 LDS 转移方程相同。

考虑按照权值采用数据结构进行优化。

先考虑一般的权值优化:如果不考虑方案计数,我们可以把一个权值树状数组作为DP结果数组,然后就可以做到 $O(\log n)$ 的状态转移:把树状数组的+ 都换成max 就可以查前后缀最值。每次查出符合题意的最大值,再修改当前位置的数字在树状数组中的 $f(i)$(显然当前的 $f(i)$ 不会比它以前的值更差(树状数组法详见排列LCS)。这样做的总复杂度为 $O(n \log n)$。 此处要求统计方案数目,而且要求“看上去一样”的方案只算一种。而普通树状数组只能返回一个满足题意的最大值。可以修改树状数组,使得它对于某个区间最大值,可以返回所有支持的坐标,但是有些麻烦。这里介绍另一种优化的方案:单调栈。

定理1:如果应用权值单调栈来优化“取出某个序列中,下标大于某个值的元素取值的最大值”这一操作,则可以令权为单调栈的元素值,原来序列中的取值为单调栈中下标,运用二分查找进行求解。

令 $S[:f(i):]= i$,其中 $S$ 是一个从栈底到栈顶单调递减的单调栈(此时维护单调性的方式与一般的单调栈不同,见下),$sz$ 为这个单调栈的大小。由上述转移方程可知,当$S[:t+1:]$ 有值时,$S[:t:]$ 也一定已经赋值。因此这是一个栈。在这个栈中,lower_bound(k)-1 这个下标即为大于数字 $k$ 的数字对应的 $f(k)$ 的最大值。每次在$[1,sz]$ 中二分查找出lower_bound(k)-1,根据状态转移方程,更新栈中 lower_bound(k) 处的值即可。由于采用的操作是查lower_bound(),如果在修改之前栈单调,修改后栈也仍然单调,即栈始终是单调的(开始时栈为空,显然是单调的)。二分查找的复杂度为 $O(\log n)$ ,因此,这个方法的总复杂度是$O(n \log n)$。

考虑多解

考虑:如何进行多解的判断?

令当前的数字为$i$,则对应的状态是$f(i)$。

进行多解的判断,就是要能遍历处于单调栈的 $f(i)-1$ 位置对应的所有数字。于是对于单调栈的每个下标,拉一条链表,保存以该下标为长度的 LDS 的末尾数字,以及以每种数字结尾的种类数目。 拉链表后如何维护栈的单调性?在栈里面放链表中最大的数字即可。在数学课上面我们接触过恒成立问题,这里也是一样:对于原来序列中在当前遍历到的位置之后的某一个数字,如果链表中存在一个数字大于它,那么这个链表中最大的数字恒大于它。这样就保证找到的 $f(j)$ 一定是最大的,即保证不漏解。

接着来统计方案数目,如果链表非空,方案数即为以链表中各个数字结尾的方案数之和,如果链表为空,就只有一种方案。注意如果链表中某个数字小于等于当前数字,那么就应该跳过它。因为我们在栈中放到是链表中最大的数字,可以保证栈中对应位置数字大于当前数字,但是不能保证链表中每个数字都大于当前数字,和当前数字一起构成LDS。在统计方案之后,更新栈中$f(i)$的值:把统计的方案数目和此时的数字插入链表头部即可。最后进行一次去重,保证以相同数字结尾的方案,在以后只算一次。

考虑:如何实现去重?

定理2:每条从栈拉出来的链都是从头到尾单调递减的。 如果位置 $i$ 对应原序列中的值为 $W_i$,那么一定有$\forall j \in \{p|p>i 且 f(p)=f(i)\} , W_j \geq W_i$。

证明:由LDS性质可知,$j>i 且 W_j<W_i \Rightarrow f(j) \geq f(i)+1$。于是后插入某条链的数字,一定大于等于先插入链的数字,即每条拉出来的链单调。

有了单调的前提就可以做到$O(n)$直接去重,遍历链表,比较当前元素和链上下个元素,如果二者相同就去掉下一个,因为下一个元素比当前元素先插入链表,即在原序列中下个元素靠前,所以当前元素包含的方案数目不比下个元素包含的少。去重后更新栈大小,为下次二分查找做准备。

实现

S 为单调栈,head[p] 为栈中下标p对应链表的第一个元素在原序列中的下标,cnt[i] 代表以原序列下标i处元素结尾的LDS的方案数目,con[i] 代表链表中的“下一个元素”,如上述操作即可。 注意$2^{31}=2147483648>INT_{MAX}$,需要用unsigned

具体代码如下:

#include <cstdio>
#include <algorithm>
#include <functional>
using namespace std;
const unsigned MAXN = 5000, INF = 0x3f3f3f3f;
unsigned N,tot,sz,W[MAXN+10],S[MAXN+10],head[MAXN+10],cnt[MAXN+10],con[MAXN+10];
int main(){
	scanf("%u",&N);
	for(int i=1;i<=N;i++)
		scanf("%u",&W[i]);
	for(int i=1;i<=N;i++) {
		unsigned pos=lower_bound(S+1,S+sz+1,W[i],greater<unsigned>())-S-1;
		tot=0;
		for(int j=head[pos];j;j=con[j])
			if(W[j]>W[i]) //要接的位置符合单调递减
				tot+=cnt[j];
		if(tot==0) tot=1;
		pos++;//转到当前处理的位置
		con[i]=head[pos];head[pos]=i;
		cnt[i]=tot;
		for(int j=head[pos];j;j=con[j])
			if(W[j]==W[con[j]]) cnt[con[j]]=0;
		S[pos]=W[i];
		sz=max(sz,pos);
	}
	tot=0;
	for(int j=head[sz];j;j=con[j])
		tot+=cnt[j];
	printf("%u %u",sz,tot);
} 

(代码短,思维量大…)

解法2

这是一个利用上面部分思路的 $O(n^2)$ 方法。

思路

由上可得:对于某个位置$i$,我们显然可以求得一个LDS长度$f(i)$。对于它前面的一个位置$j$,如果有$f(j)=f(i)-1$,那么对于相同的$W_j$,只累加最靠近 $i$ 的位置 $j$ 对应的方案数目。

实现

用一个set 维护是否累加过某个数值,每次求过LDS后,从后向前扫描即可。

代码:

#include <cstdio>
#include <set>
using namespace std;
const int MAXN = 5000;
unsigned N,W[MAXN+10],opt[MAXN+10],cnt[MAXN+10];
int main(){
	scanf("%u",&N);
	for(int i=1;i<=N;i++) scanf("%u",&W[i]);
	W[++N]=0;
	for(int i=1;i<=N;i++)
		opt[i]=1;
	for(int i=1;i<=N;i++) {
		set<unsigned> vis;
		for(int j=1;j<=i-1;j++)
			if(W[j]>W[i])
				opt[i]=max(opt[i],opt[j]+1);
		for(int j=i-1;j>=1;j--)
			if(W[j]>W[i] && opt[j]+1==opt[i] && vis.count(W[j])==0) {
				cnt[i]+=cnt[j];vis.insert(W[j]);
			}
		if(!cnt[i]) cnt[i]=1;
	}
	printf("%u %u",opt[N]-1,cnt[N]);
}