题目描述:
给定一个数组N个正整数,给每个数一个符号,正或者负使得所有数的的和的绝对值尽可能大。也就是使得这个
val(A, S) = |
sum
{ A[i]*S[i] for i = 0..N−1 }尽可能大s[i] = +1或者-1。
原题英语说了一大堆,但是实际上把所有的数当做正数的话,其实就是分成两组(一组可能为空),使得差得绝对值尽可能小。这是经典的背包问题……经典的NPC,经典的伪多项式算法。
再看数据范围:
N [0..20000] 每个数范围[-100,+100]。
要求的时间复杂度:
O(N*max(abs(A))
2
);
要求的空间复杂度:
O(N+sum(abs(A)));
数的个数很多,但数据范围很小,又有那么诡异的复杂度要求……这就是说我们可以类似基数排序那样统计一下每个数出现多少次。然后怎么做背包呢?首先把所有的数当做正数,总和是sum, 我们只要记录能达到的不超过sum/2的最大值就可以了。那么如何打表?我们记录dp[i][j]表示只使用前i种数,达到总和j的时候,最多还能剩余多少个第i种数,如果dp[i][j] = -1说明我们无法用前i种数凑够j。第i种数有have[i]个。(其实i = 0..100)
那么我们有 dp[i][j] = have[i] if dp[i - 1][j] >= 0, 因为我们用前(i - 1)种数已经达到总和j了,第i种数可以一个都不用
dp[i][j] = max(dp[i][j - i] - 1, - 1); 这是因为我们使用了第i种数,要多使用一个。这里涵盖了-1的情况。
注意更新的顺序由小到大,并且第一维可以省略掉,也就是传说中得滚动数组,不然空间复杂度达不到要求。
代码如下:
int solution(const vector<int> &A) {
// write your code here...
int i,j, sum = 0, M = 0;
for (i = 0; i < A.size(); ++i) {
int x = (A[i] >= 0)?A[i]:(-A[i]);
if (x) {
sum += x;
if (x > M) {
M = x;
}
}
}
if (M == 0) {
return 0;
}
vector<int> have;
have.resize(M + 1, 0);
for (i = 0; i < A.size(); ++i) {
++have[(A[i] >= 0)?A[i]:(-A[i])];
}
int s = (sum >> 1) + 1;
vector<int> dp;
dp.resize(s, -1);
dp[0] = 0;
int can = 0;
for (i = 1; i <= M;++i) {
if (have[i]) {
for (j = 0; j < s; ++j) {
if (dp[j] >= 0) {
dp[j] = have[i];
if (j > can) {
can = j;
}
}
else if ((j >= i) && (dp[j - i] > 0)) {
dp[j] = dp[j - i] - 1;
if (j > can) {
can = j;
}
}
}
}
}
return sum - (can << 1);
}
简单分析下,
时间复杂度: 统计部分O(N), dp外层循环不超过M次,内层循环是s次,那总复杂度是O(M * s),而s = O(sum) =O(M * N),总复杂度是O(N * M^2)。 其实再细致点分析外层循环也就是O(min(M,N))那么多次,但是没必要了。
空间复杂度:have数组O(M),dp数组O(sum),复杂度O(sum),没搞明白它那个O(N)是啥,感觉上是O(min(M,N))