划分两个元素之和相等的集合(可丢弃部分元素)

有个学弟问了我这样一个问题:
如图所示,如何计算“严教”技能在最少弃牌的情况下,划分两组点数之和相等的牌的方案?

在这里插入图片描述
在这里插入图片描述
我们把这个问题稍微整理一下,其实就是这样一个题目:

现有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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值