1. 程式人生 > >動態規劃之多重部分和問題

動態規劃之多重部分和問題

                       

  分析:拿到這道題目,當然可以用遞迴的形式進行深度搜索遍歷每種情況,但是效率會非常低。一般遞迴帶有前後數值關係的遞迴是可以用動態規劃轉化的,效率會提高很多。

  至於動態規劃,最重要的就是準確定義動態陣列的意義並找準前後的遞推式.我們這裡定義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:至於動態規劃的題目,重點在於定義好動態陣列的意義和找準遞推式,這個沒辦法,這個的話就只能多做題多感悟了