动态规划之多重部分和问题

                        《动态规划之多重部分和问题》

  分析:拿到这道题目,当然可以用递归的形式进行深度搜索遍历每种情况,但是效率会非常低。一般递归带有前后数值关系的递归是可以用动态规划转化的,效率会提高很多。

  至于动态规划,最重要的就是准确定义动态数组的意义并找准前后的递推式.我们这里定义dp[i][j]为bool类型,其意义表示为用前i种数(下标为0~i-1)能否凑成和j,能的话则dp[i][j]=true,反之则dp[i][j]=false,那么根据其前一种状态dp[i-1][j-k*a[i-1]](0<=k<=m[i-1]),如果dp[i-1][j-k*a[i-1]]=true,那么对于第i个数a[i-1]我们只要再用k个,那么就可以用前i个数凑成和为j。这是很容易理解的。递推关系式为:

                                                      《动态规划之多重部分和问题》

具体的代码如下:

#include<iostream>
using namespace std;
#define Max_N 100
#define Max_K 100000
bool dp[Max_N+1][Max_K+1];
int a[Max_N];
int m[Max_N];
int n,K;

void solve()
{
	dp[0][0]=true;
	for(int j=1;j<=K;j++)
	  dp[0][j]=false;          //用前0种数凑成和不为0的,肯定不行
	  
  //dp[i][j] 用前i种数(下标0~i-1)能不能凑成和为j 
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=K;j++)
		{
			//下标为i-1的数使用k次 
			for(int k=0;k<=m[i-1] && k*a[i-1]<=j;k++)
			{
				//不断或等 
				dp[i][j] |= dp[i-1][j-k*a[i-1]];
			}
		}
	} 
	if(dp[n][K]) cout<<"Yes\n";
	else cout<<"No\n";
	return ;
}
int main()
{
	cin>>n>>K;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	
	for(int i=0;i<n;i++)
	{
		cin>>m[i];
	}
	solve();
}

其实大家也能看到这个程序的缺点就在于三重循环下,效率还是比较低,那么我们就想着有没有更高效的方法,那么我们就可以使用另一种定义方法dp[i][j]表示:用前i种数(下标0~i-1)在能凑成和为j时,第i个数(下标为i-1)最多可以剩余多少个,-1表示前i种数凑不成和为j,递推关系式如下(每段的意义我将会写在程序的注释中):

           《动态规划之多重部分和问题》

具体的代码和递推式的意义如下:

#include<iostream>
using namespace std;
#define Max_N 100
#define Max_K 100000
int dp[Max_N+1][Max_K+1];
int a[Max_N];
int m[Max_N];
int n,K;

void solve()
{
	dp[0][0]=0;
	for(int j=1;j<=K;j++)
	  dp[0][j]=-1;          //用前0种数凑成和不为0的,肯定不行
	  
  //dp[i][j] 用前i种数(下标0~i-1)在能凑成和为j时,第i个数(下标为i-1)
  //最多可以剩余多少个,-1表示前i种数凑不成和为j 
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=K;j++)
		{
			if(dp[i-1][j]>=0)     
			//如果前i-1种数就能凑成和为j,那么第i个数a[i-1]可以一个都不用,全部剩下来
			    dp[i][j]=m[i-1];
			else             //前i-1种数凑不成和为j,那么就用第i个数a[i-1]尝试凑
			{
	//如果要凑的和j小于a[i-1],那么肯定凑不成j   ------   j<a[i-1]
	//无法用前i个数凑不成和j-a[i-1],那么无论再加不加上一个数a[i-1],都凑不成和j  -----dp[i][j-a[i-1]<0
	//如果在用前i个数能凑成和j-a[i-1],但是这时候a[i-1]数用完了,那么也是凑不成的 ------dp[i][j-a[i-1]=0
				if(j<a[i-1] || dp[i][j-a[i-1]]<=0)
				 dp[i][j]=-1;
				else
	//在前i种数能凑成和j-a[i-1]并且数a[i-1]还有剩余,那么是能凑成的,再用掉一个a[i-1],所以个数减一 
				 dp[i][j]=dp[i][j-a[i-1]]-1; 
			} 
			//cout<<"dp["<<i<<"]["<<j<<"]:"<<dp[i][j]<<endl;   //测试使用 
		}
	} 
	//dp[n][K]>=0表明前n个数(a[0]~a[n-1])在凑成和为K时,数a[i-1]不剩余或者还有剩余,
	//但是不管剩余还是不剩余,都是能凑成和为j 
	if(dp[n][K]>=0) cout<<"Yes\n";
	else cout<<"No\n";
	
	return ;
}
int main()
{
	cin>>n>>K;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	
	for(int i=0;i<n;i++)
	{
		cin>>m[i];
	}
	solve();
}

PS:至于动态规划的题目,重点在于定义好动态数组的意义和找准递推式,这个没办法,这个的话就只能多做题多感悟了

点赞