1. 程式人生 > >Partition Equal Subset Sum 分割等和子集

Partition Equal Subset Sum 分割等和子集

給定一個只包含正整數非空陣列。是否可以將這個陣列分割成兩個子集,使得兩個子集的元素和相等。

注意:

  1. 每個陣列中的元素不會超過 100
  2. 陣列的大小不會超過 200

示例 1:

輸入: [1, 5, 11, 5]

輸出: true

解釋: 陣列可以分割成 [1, 5, 5] 和 [11].

示例 2:

輸入: [1, 2, 3, 5]

輸出: false

解釋: 陣列不能分割成兩個元素和相等的子集.

思路:

這道題其實可以變成是0 1 揹包問題,申請二維陣列dp[i][j](0<=i<=nums.size(),0<=j<=(sum/2),sum是陣列的和的一半),要劃分成兩半且和相等,即sum(left)=sum(right),那麼原陣列和必須是偶數,否則無法劃分。其中dp[i][j]表示從第一個元素到第i個元素是否存在能組成和為j的子集,如果可以為true,否則為false。

接下來我們來看遞推公式,看看dp[i][j]可以怎麼由子問題推導而來,先給出公式:

dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];

1:如果不考慮第i個元素,那麼情況等於前i-1個元素的情況即dp[i-1][j](前i-1個元素如果已經可以劃分子集左,那麼剩下的元素直接劃分到另外一邊即子集右即可

2:如果考慮第i個元素,那麼情況等於前i-1個元素的子集和加上第i個元素的和可以組成和j,j-nums[i]表示前i-1個元素可以組成和為j-nums[i],那麼加上第i個元素nums[i],和即可j,可以組成子集。

所以是這兩種情況的或構成遞推公式,我一直覺得這道題和揹包問題的理解有點出入,因為01揹包問題是背或者不背,但一定所有的元素最後都會有結果(背或者不背),而這道題元素的背指的在左半邊子集,不背指的在右半邊子集,我們的目標是使得左半邊子集的和等於總和的一半。這樣思考才會和揹包問題對應上。

二維陣列版:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
	bool res = false;
	int sum = 0;
	int m = nums.size();
	for (int i = 0; i < m; i++) {
		sum += nums[i];
	}
	if ((sum & 1) == 1) {
		return false;
	}
	sum /= 2;
	bool **dp = new bool *[nums.size() + 1];
	for (int i = 0; i <= m; i++) {
		dp[i] = new bool[sum + 1];
	}
	for (int i = 0; i <= m; i++) {
		dp[i][0] = true;
	}
	for (int i = 1; i <= sum; i++) {
		dp[0][i] = false;
	}
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= sum; 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];
			}
		}
	}
	res = dp[m][sum];

	for (int i = 0; i <= m; i++) {
		delete[] dp[i];
	}
	delete[] dp;
	return res;        
    }
};

我們把空間複雜度優化到一維,觀察到對於內迴圈:

for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= sum; 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];
			}
		}
	}

我們其實只需要上一層i-1的狀態,所以我們把二維陣列壓縮為一維,遞推公式如下:

dp[j] =dp[j] || dp[j - nums[i - 1]];    j>=nums[i-1]

這裡還需要注意一點,內層迴圈需要從大到小,避免被覆蓋。

參考程式碼:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
	bool res = false;
	int sum = 0;
	int m = nums.size();
	for (int i = 0; i < m; i++) {
		sum += nums[i];
	}
	if ((sum & 1) == 1) {
		return false;
	}
	sum /= 2;
	bool *dp = new bool [sum + 1];
	for (int i = 1; i <= sum; i++) {
		dp[i] =false;
	}
	dp[0] = true;

	for (int i = 0; i < m; i++) {
		for (int j = sum; j > 0; j--) {
			if (j >= nums[i]) {
				dp[j] =dp[j] || dp[j - nums[i]];
			}
		}
	}
	res = dp[sum];

	delete[] dp;
	return res; 
    }
};