CF724E O ( n 2 ) O(n^2) O(n2) 做法题解
看到题面考虑网络流。新建超级源点 S S S,超级汇点 T T T,连边:
- ( S , i , p i ) , 1 ≤ i ≤ n (S,i,p_i) ,1\leq i\leq n (S,i,pi),1≤i≤n;
- ( i , T , s i ) , 1 ≤ i ≤ n (i,T,s_i) ,1\leq i\leq n (i,T,si),1≤i≤n;
- ( i , j , c ) , 1 ≤ i < j ≤ n (i,j,c),1\leq i <j\leq n (i,j,c),1≤i<j≤n。
对于样例 3 3 3,建出的图如下所示:
然后求得 S S S 到 T T T 的最大流,即为答案。
但是这样做图上的边数是 n 2 n^2 n2 级别的,显然过不了。
怎么办呢?把最大流转为最小割,考虑 DP,设 f i , j f_{i,j} fi,j 表示前 i i i 个点中有 j j j 个点与源点 S S S 的边未割掉。答案即为 min i = 0 n f n , i \min\limits_{i=0}^n f_{n,i} i=0minnfn,i。
接下来考虑状态转移方程。枚举当前节点 i i i,有 j j j 个点与源点 S S S 的边未割掉。对于节点 i i i,肯定要断掉它与 S S S 的连边,或它与 T T T 的连边。
- 若删除 i i i 与 S S S 的连边,则还需删除 i i i 与**编号比 i i i 小的那些节点中与 S S S 的边未割掉的那些节点(个数为 j j j)**的连边,割掉的总边权为 p i + c ∗ j p_i+c*j pi+c∗j。
- 若删除 i i i 与 T T T 的连边,则只需删除边 ( i , T , s i ) (i,T,s_i) (i,T,si) 即可。(因为 i i i 流到编号更大的节点再流到 T T T 的路径这里还不需要考虑)
因此状态转移方程为:
f i , j = min ( f i − 1 , j + p i + c ∗ j , f i − 1 , j − 1 + s i ) f_{i,j}=\min(f_{i-1,j}+p_i+c*j,f_{i-1,j-1}+s_i) fi,j=min(fi−1,j+pi+c∗j,fi−1,j−1+si)
接下来考虑边界:
枚举的时候, i i i 肯定从 1 1 1 到 n n n, j j j 从 0 0 0 到 n n n(注意 j j j 的值为 0 0 0 的时候,状态转移方程的变化,代码中体现了这一点)。
因为是求最小值, f f f 数组最开始全设为正无穷, f 0 , 0 f_{0,0} f0,0 设为 0 0 0。(但其实没有必要)
//CF724E
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e3 + 10;
long long c, p[N], s[N], f[N][N], ans = 1e17;
int n;
int main(){
scanf("%d%lld", &n, &c);
for(int i = 1; i <= n; ++ i) scanf("%lld", &p[i]);
for(int i = 1; i <= n; ++ i) scanf("%lld", &s[i]);
memset(f, 0x3f, sizeof(f));f[0][0] = 0;
for(int i = 1; i <= n; ++ i){
f[i][0] = f[i-1][0] + p[i];
for(int j = 1; j <= i; ++ j)
f[i][j] = min(f[i-1][j] + p[i] + c * j, f[i-1][j-1] + s[i]);
}
for(int i = 0; i <= n; ++ i) ans = min(ans, f[n][i]);
printf("%lld\n", ans);
return 0;
}
这样空间复杂度是 O ( n 2 ) O(n^2) O(n2) 不能通过。可以考虑滚动数组。
//CF724E
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e4 + 10;
long long c, p[N], s[N], f[2][N], ans = 1e17;
int n;
int main(){
scanf("%d%lld", &n, &c);
for(int i = 1; i <= n; ++ i) scanf("%lld", &p[i]);
for(int i = 1; i <= n; ++ i) scanf("%lld", &s[i]);
for(int i = 1; i <= n; ++ i) f[0][i] = 1e17;
for(int i = 1; i <= n; ++ i){
f[1][0] = f[0][0] + p[i];
//这一句拿出来,原因见上
for(int j = 1; j <= i; ++ j)
f[1][j] = min(f[0][j] + p[i] + c * j, f[0][j-1] + s[i]);
for(int j = 0; j <= i; ++ j) f[0][j] = f[1][j];
//这里j<=i,所以f[0][i+1]后面的还是无穷大
}
for(int i = 0; i <= n; ++ i) ans = min(ans, f[0][i]);
printf("%lld\n", ans);
return 0;
}
这里将 f f f 数组初始为正无穷的意义是什么?可以发现在状态转移时出现了 f i − 1 , j f_{i-1,j} fi−1,j 一项,这时 i − 1 i-1 i−1 有可能小于 j j j。再回想状态定义,这种情况是不合法的。如果这样初始化,这种不合法的情况就不会被考虑;反之如果初始化全为 0 0 0,就会出现错误。滚动数组的代码里的初始化也能起到这个效果。
所以你也可以这么初始化:
//for(int i = 1; i <= n; ++ i) f[0][i] = 1e17;
for(int i = 1; i <= n; ++ i){
for(int j = i+1; j <= n; ++ j) f[0][j] = 1e17;
f[1][0] = f[0][0] + p[i];
for(int j = 1; j <= i; ++ j)
f[1][j] = min(f[0][j] + p[i] + c * j, f[0][j-1] + s[i]);
for(int j = 0; j <= i; ++ j) f[0][j] = f[1][j];
}
甚至:
//for(int i = 1; i <= n; ++ i) f[0][i] = 1e17;
for(int i = 1; i <= n; ++ i){
f[0][i+1] = 1e17;
f[1][0] = f[0][0] + p[i];
for(int j = 1; j <= i; ++ j)
f[1][j] = min(f[0][j] + p[i] + c * j, f[0][j-1] + s[i]);
for(int j = 0; j <= i; ++ j) f[0][j] = f[1][j];
}