题意
数据范围
$1 leq T leq 20 $
$0leq nleq 100 $
$0leq a_ileq b_ileq 1000 $
$0leq w[i][j]leq 50 $
题解
解法1:最大权闭合子图
如果我们选了i,j两个位置,可以获得(w[i][j]+w[j][i])的收益,即可将每种选择视为一个拥有w[i][j]+w[j][i]的正点权的点,并连接了i与j两个拥有负权的点,这样可以构造一个二分图,答案则是这个二分图上的最大权闭合子图。
但是这题中的代价是以一个不过原点的一次函数呈现,还需要另外构造。对于每个数字为i的点,可以理解为每次选择都要付出(a_i)的代价,只要有数字i选择就要再额外付出(b_i-a_i)的代价。于是在构造时可以额外开十个点对应0~9,将每个点与其对应的数字的点连接并将容量赋值无限大,这十个点再与汇点连接,容量为(b_i-a_i)。
整理以下思路:将每个选择i,j作为一个点,源点s向该点连一条容量为(w[i][j]+w[j][i])的边,该点再向i,j连边,容量为无穷。再将每个点向汇点t连边,容量为(a_i),每个点向其对应数字连边,容量为无穷,每个数字点向汇点t连边,容量为(b_i-a_i)。根据最大权闭合子图,跑一遍网络流,用总正点权-最小割即为答案。虽然图中有(N^2)规模的点,但二分图中边数相对较少,dinic加当前弧优化跑出来也只用46ms。
以样例为例,可以作出以下图形:
/*************************************************************************
> File Name: 1.cpp
> Author: Knowledge_llz
> Mail: 925538513@qq.com
> Blog: https://www.cnblogs.com/Knowledge-Pig/
> Created Time: 2021/2/7 15:51:52
************************************************************************/
#include<bits/stdc++.h>
#define For(i,a,b) for(int i=(a);i<=(b);++i)
#define LL long long
#define pb push_back
#define fi first
#define se second
#define pr pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
int read(){
char x=getchar(); int u=0,fg=0;
while(!isdigit(x)){ if(x=='-') fg=1; x=getchar(); }
while(isdigit(x)){ u=(u<<3)+(u<<1)+(x^48); x=getchar(); }
return fg?-u:u;
}
const int maxx=5e5,inf=1e9;
char str[200];
int a[20],b[20],be[maxx],ne[maxx],to[maxx],w[maxx];
int v[200][200],s,t,n,cnt=2,e=1,now[maxx],dep[maxx];
void add(int x,int y,int z){
to[++e]=y;
ne[e]=be[x];
be[x]=e;
w[e]=z;
}
bool bfs(){
queue<int>q;
For(i,1,n*n+12) dep[i]=0;
q.push(s); dep[s]=1; now[s]=be[s];
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=be[u];i;i=ne[i]){
int go=to[i];
if(!dep[go] && w[i]>0){
dep[go]=dep[u]+1;
now[go]=be[go];
q.push(go);
}
}
}
return !dep[t]?0:1;
}
int dfs(int id,int mini){
if(id==t || !mini) return mini;
int ret=0,flow;
for(int &i=now[id];i;i=ne[i]){
int go=to[i];
if(w[i]>0 && dep[go]==dep[id]+1 && (flow=dfs(go,min(mini,w[i])))){
if(!flow) dep[go]=inf;
w[i]-=flow;
w[i^1]+=flow;
ret+=flow;
mini-=flow;
}
if(!mini) break;
}
return ret;
}
int dinic(){
int ret=0,tmp;
while(bfs()){
tmp=dfs(s,inf);
if(!tmp) break;
ret+=tmp;
}
return ret;
}
void init(){
For(i,0,e) to[i]=ne[i]=w[i]=0;
For(i,0,n*n+12) be[i]=now[i]=0;
e=1;
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
int T=read();
For(_,1,T){
int sum=0;
n=read(); s=n*n+11,t=n*n+12;
init();
if(n>0) scanf("%s",str+1);
For(i,0,9) a[i]=read(),b[i]=read();
if(!n){ printf("0
"); continue ; }
For(i,1,n) For(j,1,n) v[i][j]=read();
For(i,1,n) For(j,i+1,n){
int id=i*n+j;
// printf("%d %d %d %d
",id,i,j,v[i][j]+v[j][i]);
add(s,id,v[i][j]+v[j][i]); add(id,s,0);
add(id,i,inf); add(i,id,0);
add(id,j,inf); add(j,id,0);
sum+=(v[i][j]+v[j][i]);
}
For(i,1,n){
int sum=0,d=str[i]-'0';
add(i,t,a[d]);
add(t,i,0);
add(i,n*n+d+1,inf);
add(n*n+d+1,i,0);
}
For(i,0,9){ add(n*n+i+1,t,b[i]-a[i]); add(t,n*n+i+1,0); }
int ans=dinic();
printf("Case #%d: %d
",_,sum-ans);
}
return 0;
}
解法2:建立最小割模型。
解法1虽然很妙,将每种选择作为点建立二分图,但是忽略了很多信息,导致建图后有(N^2)级别的点,其实充分利用信息后可以发现,没必要将每种选择都作为一个点处理。我们把每个位置作为一个点,这是一个完全图,相互连边,边权为(w[i][j]+w[j][i]),每个点还有一些点权。如果我们已经选择了一些位置,即在图中选择了一个点集,我们能获得的收益即为该点集中的所有边边权之和减去该点集的点权。即为(ans=Maxmize sum_{(u,v)in E} w[i]-Totalcost)(w[i]表示边权),如果我们用逆思维,一个点集的边权和的两倍是不是可以等于该点集每个点相连所有边的边权和之和-该点集与其补集的最小割。用s[i]表示i点相连所有边的边权相加,(C[V,ar{V}])表示该点集与其补集的最小割,(sum_{(u,v)in E} w[i]=frac{1}{2}sum_{x in V} s[i] -C(V,ar{V})+Totalcost)。
即可得(-2 ans=Minmize C(V,ar{V})-sum_{x in V} s[i] +2Totalcost)。
于是转换为求最小割问题,每个点相互连边,容量为(w[i][j]+w[j][i]),源点向每个点连边,容量为0,每个点向汇点连边,容量为(2a_i-s_i),每个点向其对应数字的点连边,容量为无穷,对应数字向汇点连边,容量为(2(b_i-a_i))。由于边权可能出现为负的情况,每个点连接源点和连接汇点的容量同时加一个大数U,最终答案再减去n*U,最后除以-2即可。
这样建模最多只有112个点,点的数量大大减少,最终只需31msAC。
/*************************************************************************
> File Name: 1.cpp
> Author: Knowledge_llz
> Mail: 925538513@qq.com
> Blog: https://www.cnblogs.com/Knowledge-Pig/
> Created Time: 2021/2/7 15:51:52
************************************************************************/
#include<bits/stdc++.h>
#define For(i,a,b) for(int i=(a);i<=(b);++i)
#define LL long long
#define pb push_back
#define fi first
#define se second
#define pr pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
int read(){
char x=getchar(); int u=0,fg=0;
while(!isdigit(x)){ if(x=='-') fg=1; x=getchar(); }
while(isdigit(x)){ u=(u<<3)+(u<<1)+(x^48); x=getchar(); }
return fg?-u:u;
}
const int U=5e5,inf=1e9;
char str[200];
int a[20],b[20],be[200],ne[200*200],to[200*200],w[200*200];
int v[200][200],s,t,n,e=1,now[200],dep[200];
void add(int x,int y,int z){
to[++e]=y;
ne[e]=be[x];
be[x]=e;
w[e]=z;
}
bool bfs(){
queue<int>q;
For(i,1,t) dep[i]=0;
q.push(s); dep[s]=1; now[s]=be[s];
while(!q.empty()){
int u=q.front(); q.pop();
for(int i=be[u];i;i=ne[i]){
int go=to[i];
if(!dep[go] && w[i]>0){
dep[go]=dep[u]+1;
now[go]=be[go];
q.push(go);
}
}
}
return !dep[t]?0:1;
}
int dfs(int id,int mini){
if(id==t || !mini) return mini;
int ret=0,flow;
for(int &i=now[id];i;i=ne[i]){
int go=to[i];
if(w[i]>0 && dep[go]==dep[id]+1 && (flow=dfs(go,min(mini,w[i])))){
if(!flow) dep[go]=inf;
w[i]-=flow;
w[i^1]+=flow;
ret+=flow;
mini-=flow;
}
if(!mini) break;
}
return ret;
}
int dinic(){
int ret=0,tmp;
while(bfs()){
tmp=dfs(s,inf);
if(!tmp) break;
ret+=tmp;
}
return ret;
}
void init(){
For(i,0,e) to[i]=ne[i]=w[i]=0;
e=1;
For(i,1,n+12) be[i]=now[i]=0;
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.ans", "w", stdout);
#endif
int T=read();
For(_,1,T){
n=read(); s=n+11,t=n+12;
scanf("%s",str+1);
init();
For(i,0,9) a[i]=read(),b[i]=read();
For(i,1,n) For(j,1,n) v[i][j]=read();
For(i,1,n) For(j,1,n) if(i!=j){
add(i,j,v[i][j]+v[j][i]); add(j,i,0);
}
For(i,1,n){
int sum=0,d=str[i]-'0';
For(j,1,n) if(i!=j) sum+=(v[i][j]+v[j][i]);
add(i,t,a[d]*2-sum+U);
add(t,i,0);
add(s,i,U);
add(i,s,0);
add(i,n+d+1,inf);
add(n+d+1,i,0);
}
For(i,0,9){ add(n+i+1,t,2*b[i]-2*a[i]); add(t,n+i+1,0); }
int ans=dinic();
ans=(ans-n*U)/(-2);
printf("Case #%d: %d
",_,ans);
}
return 0;
}