Vladik and cards的题解,应该算比较详细的#_#

题目来源:Codeforces Round #384 (Div. 2)E.Vladik and cards

题意:给定一个长度为n的序列,对于序列中的每个数ai均有1<=ai<=8,要求在此序列中找出一个最长的子序列,其满足性质:1.对于任意数字i,j的出现次数ci,cj,均有|ci-cj|<=1;2.对于任意数字k在此子序列中一旦出现,则必须连续。

      注意:当某一数字k没有出现在子序列中时,其对应的ck=0。

样例解释:样例1:序列为1

样例2:序列为8 7 6 5 4 3 2 1

样例3:序列为1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 8

思路:首先发现ai的值很小,可以用状压压缩一下;然后我们发现作为一道正经的状压题不可能只开这么点数组(否则开到19位都能接受,出题人不至于这么傻),所以用于DP的f 数组至少有两维。继续思考,我们发现关于ai我们确实没什么好记录的了#_#,所以我们想到是否需要开一维来维护序列。思考后发现,由于要找子序列,我们必须记录当前做到了序列的哪一位。于是我们得到的用于DP的f数组:f[1010][1<<8]。

然后我们再来想一遍这个数组的含义:f[i][j]代表当前做到序列第i位,选数情况为j时的最优解。对于选数情况我们只考虑该数是否已经被选而不去考虑其出现次数,因为若选择用f数组存储这个信息,则时空爆炸,并且由性质2我们知道,当一个数的出现次数维护完前,另一个数不会出现(具有连续性)。我们还要考虑用其他方式解决“数字出现次数”的问题。我们用一个ct数组维护其目前出现过的出现次数即可,注意保证其满足性质1。我们再单独开一个vector,在程序中命名为rec,rec[i]表示数字i出现的位置。

继续考虑,我们想到一个合法的序列必须满足性质1,而性质1告诉我们一个重要事实:任意两个数的出现次数之差的绝对值不超过1,即ci与cj要么相等,要么差的绝对值为1,用稍微数学一点的方法描述,设一个合法子序列中某数字的出现次数为x(这里默认x为出现次数较少的一个),则对于这个序列,任意数字的出现次数必为x或x+1,设出现x+1次的数有cnt个,则答案即为(x*8+cnt)。

好,现在我们要想办法求出(x*8+cnt)的最大值。

由于有两个未知数,我们先考虑如何求x,这时我们想到性质二中规定此序列中的任意一个数均具有连续性,我们可以这么想:一旦数字“1”的出现次数确定了,则其他数字的出现次数也相对确定了(只能是[x-1,x]或[x,x+1]),而确定一个数字连续出现次数的方法是什么呢?我们再次观察到n的数据范围,n最大为1000看起来比较小,而一个数的连续出现次数必定在[0,n]内;此时我们再考虑答案(x*8+cnt),首先cnt必为正,且不大于8,x亦必为正,故答案具有单调性,即若能让x更优,则(x*8+cnt)必定更优,在x最优的情况下,cnt更优,答案(x*8+cnt)亦更优。

分析到这里,我们求x的方法也明确了。由于答案具有单调性且关于x单调递增,所以我们可以二分答案。在[1,n]内取M=(1+n)>>1,设一个函数check(M)来判断M是否可行,若可行则[1,M-1]均不是最优,若不可行则[M+1,n]均不可行。

似乎都考虑完了?等等,刚刚我们说的范围是[1,n],所以我们还要考虑x=0,即有的数取0次出现次数的情况(即此数字不存在于序列中),这种情况要单独考虑,只要开个桶记录一下出现了哪些数字就可以(当然也可以利用之前的rec直接判断)。

二分答案的思想就讲到这里,下面讨论一下DP。

隔得比较远了,这里再说明一下DP数组f[1010][1<<8]的意义:f[i][j]代表当前做到序列第i位,选数情况为j时的最优解。那么我们先解决关于初始化的问题,f[0][0]必为0,它表示此时正在做第0位(还没开始做),一个数都没有出现(第二维是0),其他的f我们没有发现能直接确定的值,故给他们均赋一个较大的负数。

考虑到数字出现次数有M和M+1两种情况,我们需要两个方程。

还记得我们用一个数组ct记录了数字的出现次数吗?我们首先思考如何转移第一维,即做到第i位时如何转移。考虑记录一个h=x+ct[k]-1表示当前数字k在第i位后的出现位置,用于枚举当前位i后第h个k的位置,即rec[k][h]。这里的k只需枚举1~8,范围很小。当然我们还要保证这个数的位置存在,即要保证h<rec[k].size(),由于f[i][j]表示我们做到了第i位,而我们枚举的rec[k][h]即表示当前位i的转移状态,由于rec[k][h]从零开始,我们在方程上需要给其加1,于是我们就确定了第一维的转移。

接下来考虑第二维的转移,我们在第二维记录的是我们找到的子序列中是否包含该数,若包含则对应位为1,否则为0。由于我们有枚举数字k,所以只需要将k的对应位和原状态进行一次或运算即可,即j|(1<<k),这里的j表示枚举选数状态,范围为[0,1<<8)。我们还需要保证j中被选的数和k均不相等(否则即破坏了性质2)于是转移后我数组即为f[rec[k][h]+1][j|(1<<k)],其值为其本身和转移前的值中较大的一个,于是我们得到了DP方程:

f[rec[k][h]+1][j|(1<<k)]=max(f[rec[k][h]+1][j|(1<<k)],f[i][j])

前面提到过,我们要分两种情况讨论,所以有两条DP方程,但大同小异,列出如下:

f[rec[k][h]+1][j|(1<<k)]=max(f[rec[k][h]+1][j|(1<<k)],f[i][j])

f[rec[k][h+1]+1][j|(1<<k)]=max(f[rec[k][h+1]+1][j|(1<<k)],f[i][j]+1)

最后由于我们改变了数字的出现次数,所以转移完一次后要给当前位的出现次数加1,即ct[a[i+1]-1]++,由于i从0开始枚举,所以需要将变量写为a[i+1]。

所有的DP做完,只剩下最后一件事情:最优解在哪里。

我们知道,最优解必须将全部数字均选过至少一次(否则就变成x=0的特殊情况),所以第二维必定是(1<<8)-1,而在第一维,我们并不知道何时最优,还好第二维已确定,n的范围也不大,我们直接枚举第一维,用max函数找到最优解,即很久之前我们提到的cnt(忘记的同学可以回去翻一下),找到后我们即可以返回(x*8+cnt)了,没找到就……输出一个特殊标记值(例如-1)然后继续递归吧。

噢还有,每次check(M)时的选数情况都是不相同的,所以统计统计出现次数的ct数组每次都要清空一下。

我能帮大家分析的就到这里了,希望大家看完以上内容能自己打出代码。

当然自己的代码还是要贴的#_#。

代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#define INF 0x3f3f3f3f
using namespace std;
int n,L,R,ans;
int a[1010],ct[1010];
int f[1010][1<<8];
vector <int> rec[1010];
int check(int x)//判断M是否合法 
{
	memset(ct,0,sizeof(ct));
	for(int i=0;i<=n;i++)
	for(int j=1;j<(1<<8);j++) f[i][j]=-INF;
	f[0][0]=0;//初始化 
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<(1<<8);j++)
		if (f[i][j]!=-INF)
		for(int k=0;k<8;k++)
		if ((j&(1<<k))==0)
		{
			int h=x+ct[k]-1;
			if (h>=rec[k].size()) continue;//判断h是否存在 
			f[rec[k][h]+1][j|(1<<k)]=max(f[rec[k][h]+1][j|(1<<k)],f[i][j]);
			h++;
			if (h>=rec[k].size()) continue;
			f[rec[k][h]+1][j|(1<<k)]=max(f[rec[k][h]+1][j|(1<<k)],f[i][j]+1);//DP,详见题解					
		}
		ct[a[i+1]-1]++;//出现次数+1 
	}
	int cnt=-INF;
	for(int i=0;i<=n;i++) cnt=max(cnt,f[i][(1<<8)-1]);
	return cnt==-INF?-1:x*8+cnt;//找最优解 
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) rec[a[i]-1].push_back(i-1);//存储数字i出现的位置 
	L=1;R=n>>3;
	while(L<=R)//二分答案 
	{
		int M=(L+R)>>1;
		if (check(M)!=-1) ans=max(ans,check(M)),L=M+1;//M合法,保存其值并去除非最优解 
		else R=M-1;//M不合法,去除M及M以上的不合法解 
	}
	if (ans==0) for(int i=0;i<8;i++) if (rec[i].size()) ans++;//讨论x=0的情况 
	printf("%d",ans);
	/*for(int i=1;i<=n;i++)
	for(int j=1;j<(1<<8);j++) printf("%d ",f[i][j]);*/
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值