1519F - Chests and Keys——网络流模型优化状压DP

F - Chests and Keys

英文题面

题目描述

有n个宝箱和m种钥匙/锁,一种钥匙对应一把锁,Alice负责给箱子上锁,而Bob需要花钱买钥匙开宝箱牟利。第i个宝箱有钱 a i a_i ai ,第j种钥匙价格为 b j b_j bj ,给第i个宝箱上第j种锁要花费 c i , j c_{i,j} ci,j

求Alice最少花费多少上锁费使得在上完锁后,保证Bob开箱无法牟利(指箱中牟的钱-钥匙钱>0)

数据范围与提示

1 ≤ n , m ≤ 6 1\le n,m\le 6 1n,m6 , 1 ≤ a i , b j ≤ 4 1\le a_i,b_j\le 4 1ai,bj4 , 1 ≤ c i , j ≤ 1 0 7 1\le c_{i,j}\le 10^7 1ci,j107

思路

实在没有更好的做法,只好按自己的理解翻译官方题解了。

官方做法把问题转换成了一个难以理解的网络流模型,从原点向每个箱子 i 连有一条容量为 a i a_i ai 的边,每个钥匙 j 向汇点(官解直译:水槽)连有一条容量为 b j b_j bj 的边,若箱子 i 上了第 j 种锁,那么从箱子 i 向钥匙 j 连一条容量无限大的边,那么此方案保证Bob不赚当且仅当所有从原点到每个箱子的边全部满流。为什么呢?首先,每个箱子一定得上一把锁,这是很显然的,因为如果有箱子没锁,那么Bob可以徒手开箱,百分百牟利。所以每个箱子一定可以有路连向汇点。

然后用反证法,考虑有一条到箱子 x 的边没有满流,那么该箱子连向的钥匙集 S x S_x Sx 连向汇点的边一定满流。此时如果边的流量来源只有 x ,那么Bob可以仅用 S x S_x Sx 开宝箱 x 就可牟利;如果流量来源还有从宝箱 y 来的一部分,那么 S y S_y Sy 连向汇点的边也一定满流(否则就可以把流向 S x ∩ S y S_x\cap S_y SxSy 的流分往 S y − S x ∩ S y S_y-S_x\cap S_y SySxSy ,就矛盾了),此时Bob可以用 S x ∪ S y S_x\cup S_y SxSy 开宝箱 x、y 牟利。以此类推,以这种方式构成的连通块的所有钥匙连向汇点的边一定满流,加上有连向宝箱的边不满流,那么Bob一定可以牟利。反证法证毕,不能有任何一条到箱子的边不满流。

然后就是DP了。考虑用最暴力的DP,记录箱子与钥匙连边的状态,此时一定对应一种从原点出发的最大流的方案,然后上边的网络流就派上用场。我们可以仅记录每条到宝箱的边的流量,然后考虑枚举钥匙 j ,再枚举宝箱 i ,一条一条地给钥匙通往汇点的边増流,此时只需要记录到宝箱的 n 条边的状态、i 和 j 、钥匙 j 连向汇点的边的流量 r 这几个状态(压缩过后发现可过),每次増流往后面的状态转移一次,由于一条边流量最大为4,所以单次转移是常数。

为什么这样定义状态可以省但仍然正确呢?因为可以有多个连边状态对应一种流量-i-j-r状态,而同一种流量-i-j-r往后转移方式是固定的,所以用流量-i-j-r状态来DP可以先挤掉劣的方案再转移,正确性显然。

代码

题解复杂就结合代码理解吧。我研究官解很久,最后也是看代码看懂的。可见我英语学得还没有计算机语言好。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#define ll long long
#define MAXN 100005
#define uns unsigned
#define INF 0x3f3f3f3f
using namespace std;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+s-'0',s=getchar();
	return f?x:-x;
}
int n,m,a[10],b[10];
int c[10][10];
int dp[15630][7][7][5],ans=INF;
struct itn{int sk[7];};
inline int getcd(itn a){
	int code=0;
	for(int i=1;i<=n;i++)code=code*5+a.sk[i];
	return code;
}
inline itn getst(int c){
	itn state={0,0,0,0,0,0,0};
	for(int i=n;i>0;i--)state.sk[i]=c%5,c/=5;
	return state;
}
inline bool check(itn s){
	for(int i=1;i<=n;i++)
		if(s.sk[i]<a[i])return 0;
	return 1;
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=m;i++)b[i]=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			c[i][j]=read();
	memset(dp,0x3f,sizeof(dp));
	dp[0][1][1][0]=0;
	int M=pow(5,n);
	for(int S=0;S<M;S++){
		itn s=getst(S);bool ok=1;
		for(int i=1;i<=n;i++)if(s.sk[i]>a[i])ok=0;
		for(int j=1;ok&&j<=m;j++)
			for(int i=1;i<=n;i++)
				for(int r=0;r<5;r++){
					if(r>b[j])break;
					for(int f=0;f<5;f++){
						if(s.sk[i]+f>a[i]||r+f>b[j])break;
						itn nw=s;
						nw.sk[i]+=f;
						int ad=(f?c[i][j]:0),ni=i,nj=j,nr=r+f;
						if(i==n)ni=1,nj++,nr=0;
						else ni++;
						if(check(nw))ans=min(ans,dp[S][i][j][r]+ad);
						if(nj<=m){
							int cd=getcd(nw);
							dp[cd][ni][nj][nr]=min(dp[cd][ni][nj][nr],dp[S][i][j][r]+ad);
						}
					}
				}
	}
	if(ans>=INF)ans=-1;
	printf("%d\n",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值