单重部分和
问题描述
给定 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
1≤n≤100
1
≤
a
i
≤
100000
1 \le a_i \le 100000
1≤ai≤100000
1
≤
k
≤
100000
1 \le k \le 100000
1≤k≤100000
样例输入
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[i−1][j−k×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[i−1][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[i−1][j−a[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][j−a[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(j≥1),代码如下:
#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
1≤n≤100
1
≤
a
i
,
m
i
≤
100000
1 \le a_i,m_i\le 100000
1≤ai,mi≤100000
1
≤
k
≤
100000
1 \le k \le 100000
1≤k≤100000
样例输入
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] >= 0
−
1
,
j < a[i] || dp[j - a[i]] < 0
d
p
[
j
−
a
[
i
]
]
−
1
,
其他情况
dp[i][j] = \begin{cases} a[i], & \text{dp[i-1][j] >= 0} \\ -1, & \text{j < a[i] || dp[j - a[i]] < 0} \\ dp[j - a[i]] -1, & \text{其他情况}\end{cases}
dp[i][j]=⎩⎪⎨⎪⎧a[i],−1,dp[j−a[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
]
<
0
dp[i-1][j]<0
dp[i−1][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 时,可以获取更多的信息来降低复杂度。