题解 [CF724E] Goods transportation

Luogu Link

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),1in
  • ( i , T , s i ) , 1 ≤ i ≤ n (i,T,s_i) ,1\leq i\leq n (i,T,si),1in
  • ( i , j , c ) , 1 ≤ i < j ≤ n (i,j,c),1\leq i <j\leq n (i,j,c),1i<jn

对于样例 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+cj
  • 若删除 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(fi1,j+pi+cj,fi1,j1+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} fi1,j 一项,这时 i − 1 i-1 i1 有可能小于 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];
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值