部分和问题

单重部分和

问题描述

给定 n n n 个数,分别是 a 1 a_1 a1 a 2 a_2 a2 a n a_n an,给定一个数 k k k,要求判断在这些数中是否存在某几项的和为 k k k

问题规模

1 ≤ n ≤ 100 1 \le n \le 100 1n100
1 ≤ a i ≤ 100000 1 \le a_i \le 100000 1ai100000
1 ≤ k ≤ 100000 1 \le k \le 100000 1k100000

样例输入

3 17
3
5
8

样例输出

NO

问题求解

对于 n 个数,每一个都有两个选项,选或者不选最直接的想法是深搜,但是这样复杂度会达到 O ( 2 n ) O(2^n) O(2n),显然不行,所以可以考虑用 dp 来做。
使用 d p [ i ] [ j ] dp[i][j] dp[i][j] 数组存储,值为 1 时表示前 i i i 个数的某几项的和可以达到 j j j,值为 0 时表示不能达到,所以 d p [ i ] [ j ] dp[i][j] dp[i][j] 可以是 bool 数组,true 表示可以达到,false 表示不可以达到。
对于第 i i i 个数,有两个选择,要或者不要,所以有如下方程:
d p [ i ] [ j ] ∣ = d p [ i − 1 ] [ j − k × a [ i ] ] dp[i][j]|= dp[i - 1][j - k \times a[i]] dp[i][j]=dp[i1][jk×a[i]]
其中 k 取 0 或 1。k 为 0 时, d p [ i ] [ j ] ∣ = d p [ i − 1 ] [ j ] dp[i][j]|= dp[i - 1][j] dp[i][j]=dp[i1][j],可以得到不取第 i i i 个数时是否可以达到 k k k,k 为 1 时, d p [ i ] [ j ] ∣ = d p [ i − 1 ] [ j − a [ i ] ] dp[i][j]|= dp[i - 1][j-a[i]] dp[i][j]=dp[i1][ja[i]],可以得到取第 i i i 个数时是否可以达到 k k k,循环两次后就可以得到前 i i i 个数是否能达到 k k k
再来看初始条件,求 d p [ 1 ] [ j ] dp[1][j] dp[1][j] 时,要用到 d p [ 0 ] [ j ] dp[0][j] dp[0][j] d p [ 0 ] [ j − a [ 1 ] ] dp[0][j-a[1]] dp[0][ja[1]],当 j j j a [ 1 ] a[1] a[1] 时,可以和可以达到 j j j,所以 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] 要是 true。其他情况都是都达不到前 1 个数的和为 j j j 的,所以 d p [ 0 ] [ j ] = f a l s e ( j ≥ 1 ) dp[0][j] = false(j \ge 1) dp[0][j]=false(j1),代码如下:

#include <iostream>
using namespace std;

const int NUM = 1e5;
int dp[NUM / 1000 + 50][NUM];
int n;
int K;
int a[NUM];
int m[NUM];

void solveSingle() {
	dp[0][0] = 1;
	for (int i = 1; i <= n; ++i) {
		for (int j = 0; j <= K; ++j) {
			for (int k = 0; k <= 1 && k * a[i] <= j; ++k) {
				dp[i][j] |= dp[i - 1][j - k * a[i]];
			}
		}
	}
}

int main(int argc, char** argv) {
	cin >> n >> K;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	solveSingle();
	if (dp[n][K] == 1) cout << "YES" << endl;
	else cout << "NO" << endl;
	
	return 0;
}

算法的时间复杂度为 O ( n k ) O(nk) O(nk),达到 1 0 7 10^7 107,可以在 1 秒内完成。

多重部分和

问题描述

给定 n n n 个不同的数,分别是 a 1 a_1 a1 a 2 a_2 a2 a n a_n an,每个数分别有 m 1 m_1 m1 m 2 m_2 m2 m n m_n mn个,给定一个数 k k k,要求判断在这些数中是否存在某几项的和为 k k k

问题规模

1 ≤ n ≤ 100 1 \le n \le 100 1n100
1 ≤ a i , m i ≤ 100000 1 \le a_i,m_i\le 100000 1aimi100000
1 ≤ k ≤ 100000 1 \le k \le 100000 1k100000

样例输入

3 17
3 5 8
3 2 2

样例输出

YES

问题求解

多重部分和和单重部分和区别在于每个数都有 m i m_i mi 个数,仔细分析一下,解这道题似乎可以用前面的代码,只需要修改 solveSingle 函数里的第三重 for 循环
单重部分和代码:

for (int k = 0; k <= 1 && k * a[i] <= j; ++k) {
	dp[i][j] |= dp[i - 1][j - k * a[i]];
}

修改为:

for (int k = 0; k <= m[i] && k * a[i] <= j; ++k) {
	dp[i][j] |= dp[i - 1][j - k * a[i]];
}

这样就可以解这道题了,但是去无法应对这道题目的数据,算法的时间复杂度是 O ( ∑ 1 n k ) O(\sum_1^n k) O(1nk),以这道题的数据规模,最大可以达到 1 0 12 10^ {12} 1012,完全无法在规定的时间完成要求,所以我们要寻找更有效率的算法。
思考一下,以 bool 数组记录,对于每一个 j j j,都会循环 m i m_i mi 次,如果可以将这个开销去掉,就可以提高算法的速度,如何去掉这个开销呢?我们可以记录每次取完一个 a i a_i ai 后还剩多少个 a i a_i ai,这样可以实现提高算法速度了。有如下方程:
d p [ i ] [ j ] = { a [ i ] , dp[i-1][j] &gt;= 0 − 1 , j &lt; a[i] || dp[j - a[i]] &lt; 0 d p [ j − a [ i ] ] − 1 , 其他情况 dp[i][j] = \begin{cases} a[i], &amp; \text{dp[i-1][j] &gt;= 0} \\ -1, &amp; \text{j &lt; a[i] || dp[j - a[i]] &lt; 0} \\ dp[j - a[i]] -1, &amp; \text{其他情况}\end{cases} dp[i][j]=a[i],1,dp[ja[i]]1,dp[i-1][j] >= 0j < a[i] || dp[j - a[i]] < 0其他情况
再来看初始情况,当 j = a [ i ] j = a[i] j=a[i] 时, d p [ i ] [ j ] = m [ i ] dp[i][j]=m[i] dp[i][j]=m[i] d p [ i − 1 ] [ j ] &lt; 0 dp[i-1][j]&lt;0 dp[i1][j]<0的情况下),所以要求 d p [ 0 ] [ j ] = m [ i ] dp[0][j]=m[i] dp[0][j]=m[i]。代码如下:

#include <iostream>
using namespace std;

const int NUM = 1e5;
int dp[NUM / 1000 + 50][NUM];
int n;
int K;
int a[NUM];
int m[NUM];

void solveMultiple() {
	for (int i = 1; i <= K; ++i) dp[0][i] = -1;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= K; ++j) {
    		if (dp[i - 1][j] >= 0) {
    			dp[i][j] = m[i];
			}
    		else if (j < a[i] || dp[i][j - a[i]] < 0) {
        		dp[i][j] = -1;
			}
			else {
    			dp[i][j] = dp[i][j - a[i]] - 1;
			}
		}
	}
}

int main(int argc, char** argv) {
	cin >> n >> K;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	for (int i = 1; i <= n; ++n) {
		cin >> m[i];
		dp[i][0] = m[i];
	}
	solveMultiple();
	if (dp[n][K] >= 0) cout << "YES" << endl;
	else cout << "NO" << endl;
	
	return 0;
}

时间复杂度是 O ( n k ) O(nk) O(nk),可以解决这个问题。

总结

多重部分和将 d p dp dp 数组不再只记录是否可以达到 j,而是记录达到 j 时还剩多少个 a [ i ] a[i] a[i],这样就将复杂度降下来了,有时候 dp 时,可以获取更多的信息来降低复杂度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值