1. 程式人生 > >LeetCode416 分割等和子集

LeetCode416 分割等和子集

       主要的思想來源於0/1揹包問題,解決方法是動態規劃。我們可以想到,把陣列分割成兩份,並且和相等,那麼每一份的和是總和的一半。那麼問題就可以轉化為找到一組數,使得他們的和逼近sum/2,最後判斷最大的和是否等於sum/2,如果是則說明存在這樣的組合,也就是存在子集。其和為sum/2,當然了另一個集合的和也就是sum/2。注意到如果陣列的總和為奇數,則可以直接判斷不存在這樣的子集。所以在正式程式開始前可以先判斷陣列總和是否是奇數。

      0/1揹包問題的公式為               f(n,y)=\begin{cases} p_{n} & \text{if } y\geqslant w_{n} \\ 0& \text{ if } y<w_{n} \end{cases}

                                                           f(i,y)=\begin{cases} max(f(i+1,y),f(i+1,y-w_{i})+p_{i}) & \text{ if } y\geqslant w_{i} \\ f(i+1,y) & \text{ if } 0\leqslant y< w_{i} \end{cases}

其中f(n,y)為總價值,wi為貨物重量,pi為單個價值的重量,y為貨車容量,我們的目標是在不超過貨車容量的前提下使得所裝的貨物總價值最高。

     解決此類問題有兩種解決方法,一種是直接利用遞迴方程求解,但是會有重複計算,可以建立一個二維陣列記錄數值,令其初始值為-1,還可以避免重複計算。另一中方法是迭代程式代替遞迴,避免了重複計算,速度相對較快,但是所受侷限就是權值必須為整數。本題我們分別選擇遞迴程式和迭代程式來做,直接利用上述公式,可得下面程式碼

方法一:遞迴程式

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.size()==1){  //不可能分成兩個子集
            return false;
        }
        int sum=accumulate(nums.begin(),nums.end(),0);
        if(sum%2){
            return false;
        }
        int n=nums.size(),cap=sum/2;
         int** cArray=new int*[n];
         for(int i=0;i<n;i++){
             cArray[i]=new int[cap+1];
             for(int j=0;j<cap+1;j++){
              cArray[i][j]=-1;
             }
         }
        int re=f(0,cap,nums,cArray);
         for(int i=0;i<n;i++){
             delete [] cArray[i];
         }
        delete cArray;
        return re==sum/2;
    }
    int f(int i,int cap,vector<int>& nums,int** cArray)
    {
        if(cArray[i][cap]>=0) {
            return cArray[i][cap];
        }
        if(i==nums.size()-1){
            cArray[i][cap]=cap>=nums[i]?nums[i]:0;
            return cArray[i][cap];
        }
        if(cap<nums[i]){
            cArray[i][cap]= cArray[i+1][cap];
        }else{
            cArray[i][cap]=max(f(i+1,cap,nums,cArray),f(i+1,cap-nums[i],nums,cArray)+nums[i]);
        }
        return cArray[i][cap];
    }
};

方法二 ,迭代程式 

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum=accumulate(nums.begin(),nums.end(),0);
        if(nums.size()==1||sum%2==1){
            return false;
        }
        int cap=sum/2;
        int n=nums.size();
        int f[n][cap+1];
        int ymax=min(nums[n-1]-1,cap);
        //初始化fn,y表示剩餘容量,總的容量為sum/2
        for(int y=0;y<=ymax;y++)
        {
            f[n-1][y]=0;
        }
        for(int y=nums[n-1];y<=cap;y++)
        {
            f[n-1][y]=nums[n-1];
        }
        for(int i=n-2;i>0;i--)
        {
            ymax=min(nums[i]-1,cap);
            for(int y=0;y<=ymax;y++)
            {// y<Wi 表示容量不夠
                f[i][y]=f[i+1][y];
            }
            for(int y=nums[i];y<=cap;y++)
            {//y>=wi
                f[i][y]=max(f[i+1][y],f[i+1][y-nums[i]]+nums[i]);
            }
        }
        //i=0,上面的迭代計算會這一步計算做準備
        if(cap>=nums[0])
        {//有機會取到第一個值
            f[0][cap]=max(f[1][cap],f[1][cap-nums[0]]+nums[0]);
        }else{  //沒有機會取到第一個值
            f[0][cap]=f[1][cap];
        }
        return f[0][cap]==cap;  //是否裝滿貨車
    }
};

方法三,迭代程式的簡化版,程式碼如下,主要是把二維空間壓縮到一維

class Solution {
public:
    bool canPartition(vector<int>& nums) {
	int sum =accumulate(nums.begin(),nums.end(),0);
        if(nums.size()==1||sum%2==1){
            return false;
        }
	int n= nums.size();
	int cap=sum/2;
	bool dp[cap+1];
    //初始化
	for (int i = 1; i <=cap; i++) {
		    dp[i] =false;
	}
	dp[0] = true; 
	for (int i = 0; i<n; i++) {
		for (int j =cap; j > 0; j--) {
			if (j >= nums[i]) {
				dp[j] =dp[j] || dp[j - nums[i]];//要麼用第j個數,要麼不用
			}
		}
	}
	return dp[cap];
    }
};

壓縮思想來源於下面的程式碼,dp[i][j]和dp[i-1][j]的關係是層的關係,後者是前者的上一層,所以外層由i控制就夠了,主要比較的j這一層的值。為了防止覆蓋,要從大的值開始,相當於之前左邊是i,右邊是i-1,現在變成左邊是i,右邊是i+1,這和之前的揹包一致。

for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= sum/2; j++) {
			if (j >= nums[i - 1]) {
				dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
			}
			else {
				dp[i][j] = dp[i - 1][j];
			}
		}
	}

其中dp[i][j]表示從第一個元素到第i個元素是否存在能組成和為j的子集,如果可以為true,否則為false。

綜合比較,簡化版程式程式碼減少了,但是理解起來有點困難,而且“揹包意圖”不太明顯。第一版程式雖然複雜,但是理解起來容易,而且如果程式要求具體的陣列集合,則第一版在計算完f(i,y)後可以直接確定權值(0/1)。