有个学弟问了我这样一个问题:
如图所示,如何计算“严教”技能在最少弃牌的情况下,划分两组点数之和相等的牌的方案?
我们把这个问题稍微整理一下,其实就是这样一个题目:
现有N个元素分别为num[i],且1≤num[i]≤13,在最多丢弃一个元素的情况下,将它们划分为两个元素之和相等的集合,输出所有方案。
看到这种题目,我整个人都直接暴力了起来。 毕竟牌数很少,不用伤神费脑细胞,直接暴力求解是最方便的。
后来学弟拉住我说哥算了算了, 我稍加思索感觉好像有几分动态规划的意思在里面,然后仔细一想这不就是01背包问题在刚好装满时候的动态规划问题么……
我啪地一下就站了起来,很快啊, 写出状态转移方程:
其中dp[i][j]是从前i个元素中选取若干个,能刚好使得元素之和为j的方案数量。
那么如何求解具体方案呢?
也很简单,在动态规划求解完毕之后,从dp[i_max][j_max]递归回去输出方案就可以了。
于是整个思路就非常清晰:
1、所有元素之和为偶数,有可能可以直接划分;
(1)若能直接划分,结束。
(2)若不能直接划分,去掉一个偶数元素再进行尝试划分,无论是否划分成功均结束。
2、所有元素之和为偶数,显然不可能直接划分,必须去掉一个奇数元素再进行尝试划分,无论是否划分成功均结束。
显然,这里的划分就是求解当元素之和为sum/2时的动态规划了。
求解dp的时间复杂度是O(N*sum/2),但是由于sum和N之间是小常数关系,可以认为时间复杂度是O(N²)。至于递归整理具体方案的过程,由于有了dp[][]的记忆实现了剪枝,因此时间复杂度可以认为是线性的,相比求解dp可以略去。
考虑到有可能需要去掉一个元素尝试划分,所以最终总体的最坏时间复杂度为O(N³)。
PS:
1、为什么用cstdio不用iostream?
因为流输入输出在数据量极大的时候会明显慢于scanf和printf,打比赛的时候可能因为这个直接超时,所以现在写cstdio成习惯了……
2、如果想求解最多可以丢弃N-1个元素的情况怎么办?
其他不用改,在代码里面添加改动M和数组tmp[][]的程序就可以了。
3、为什么不对dp使用滚动数组优化把二维数组变成一维数组?
因为要递归打印具体方案,所以如果滚动数组优化了,就没有了记忆,递归的时间复杂度会大大提升,最坏情况下将会是O(2^N)。
4、为什么会多次打印重复的元素方案?
因为一开始考虑的时候是按照打牌考虑的,即使点数相同,牌的功能也是不一样的,所以即使点数相同也认为是两个不同的元素……如果想不重复输出,只要把具体方案card[]每次print之前算个hash值出来,如果和以前的hash值相同就不再输出就行了。
具体代码如下:
#include<cstdio>
int N,M;//N是元素总数量,M是参与划分的元素数量
int i,j,k;
int num[100],tmp[100];//num是所有元素,tmp是参与划分的元素
int dp[100][1500];
int sum,half;//sum为所有元素之和,half为参与划分的元素之和的一半
int card[100],cnt=0;//card存储具体方案,cnt为具体方案的元素数量
int prt()//输出划分方案
{
int i,j;
for(i=cnt;i>=1;i--)
printf("%d ",card[i]);
printf("\n");
return 0;
}
int find(int i,int j)//递归整理划分方案
{
if(i==0&&j==0)
prt();
else
{
if(i==0) return 0;
if(j<tmp[i])
{
if(dp[i-1][j]>0) find(i-1,j);
}
else
{
cnt++;
card[cnt]=tmp[i];
if(dp[i-1][j-tmp[i]]>0) find(i-1,j-tmp[i]);
cnt--;//恢复现场
if(dp[i-1][j]>0) find(i-1,j);
}
}
}
int solve()
{
int i,j;
dp[0][0]=1;
for(i=1;i<=M;i++)
for(j=0;j<=half;j++)
if(j<tmp[i])
dp[i][j]=dp[i-1][j];
else
dp[i][j]=dp[i-1][j]+dp[i-1][j-tmp[i]];
return 0;
}
int main()
{
scanf("%d\n",&N);
sum=0;
for(i=1;i<=N;i++)
{
scanf("%d",&num[i]);
sum+=num[i];
}
if(sum%2==0)//如果是所有元素之和是偶数
{
half=sum/2;
M=N;
for(i=1;i<=N;i++)
tmp[i]=num[i];
solve();
if(dp[M][half]>0) find(M,half);//所有元素直接划分成功
else //划分不成功
{
M=N-1;
for(j=1;j<=N;j++)
if(num[j]%2==0)
{
half=(sum-num[j])/2;
i=0;
k=0;
while(k<N)
{
k++;
if(k!=j)
{
i++;
tmp[i]=num[k];
}
}
printf("Discard No.%d card %d: \n",j,num[j]);
solve();
if(dp[M][half]>0) find(M,half);
else printf("No Answer!\n");
}
}
}
else//如果是所有元素之和是奇数
{
M=N-1;
for(j=1;j<=N;j++)
if(num[j]%2==1)
{
half=(sum-num[j])/2;
i=0;
k=0;
while(k<N)
{
k++;
if(k!=j)
{
i++;
tmp[i]=num[k];
}
}
printf("Discard No.%d card %d: \n",j,num[j]);
solve();
if(dp[M][half]>0) find(M,half);
else printf("No Answer!\n");
}
}
return 0;
}