P2774 方格取数问题 题解
Post time: 2020-02-24 09:40:26
这道题目解法很妙,主要用到了最大和 \(=\) 总和 \(-\) 最小舍弃和。
最小舍弃和即最小割(我们构造出一个二分图满足割掉 \(=\) 舍去),我们又知最小割 \(=\) 最大流,所以是一道最大流问题。
考虑建图:
我们发现,只要两个点相邻(不兼容),那么这两个点的横纵坐标和的奇偶性一定不同,即 \((x_i+y_i)\bmod 2 \neq (x_j+y_j)\bmod 2\),所以我们可以把整个图分成 \((x_i+y_i)\bmod 2=0\) 的点集和 \((x_i+y_i)\bmod 2=1\) 的点集。这样就构成了二分图。
考虑构图:
将这两个点集分别全部连向 \(S\) 和 \(T\)(边权即为此方格中的数),中间相邻点连边(边权为 \(INF\))。这样我们可以保证形成最小割时,图的中间所有边都没被删,也就达成了割掉 \(=\) 舍去这一目的。最终,跑最大流即可。
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
const int N=20000+21,INF=0x3f3f3f3f;
struct Edge{int u,v,w,nxt;}e[N*100];
struct Node{int u,val;bool operator <(const Node &a)const{return val<a.val;};};
priority_queue<Node>q;//HLPP用优先队列优化
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int h[N],gap[N],flow[N],d[N];
bool vis[N];
int n,m,s,t,sum,tot=1;
inline int getnum(int x,int y){return (x-1)*m+y;}
inline bool check(int x,int y){return x>=1&&x<=n&&y>=1&&y<=m;}
inline void add(int u,int v,int w){//别忘了反向边
e[++tot]=(Edge){u,v,w,h[u]};h[u]=tot;
e[++tot]=(Edge){v,u,0,h[v]};h[v]=tot;
}
bool bfs(){
memset(d,-1,sizeof(d));
queue<int>Q;
Q.push(t);//HLPP反向建边
d[t]=0;
while(!Q.empty()){
int u=Q.front();
Q.pop();
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i^1].w;//注意反向边
if(d[v]==-1&&w){
d[v]=d[u]+1;
Q.push(v);
}
}
}
return d[s]!=-1;
}
void Init(){//队列初始化
for(int i=h[s];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(w){
e[i].w=0,e[i^1].w=w;
flow[s]-=w,flow[v]+=w;
if(v!=s&&v!=t&&!vis[v]){
vis[v]=1;
q.push((Node){v,d[v]});
}
}
}
}
void Push(int u){//扫描到u点,把u点的流往下推进
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(d[u]==d[v]+1&&w){
int f=min(flow[u],w);
e[i].w-=f,e[i^1].w+=f;
flow[u]-=f,flow[v]+=f;
if(v!=s&&v!=t&&!vis[v]){
vis[v]=1;
q.push((Node){v,d[v]});
}
}
}
}
void Gap(int u){//断层优化,把所有点的层数都改为源点+1,使其尽快流回源点
for(int i=1;i<=n*m;++i){
if(i!=s&&i!=t&&d[i]>d[u]&&d[i]<=n) d[i]=n+1;
}
}
void Relabel(int u){//把u点的层设为它连向的点中最小的层数+1,保证能流
d[u]=INF;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(w) d[u]=min(d[u],d[v]);
}
d[u]++;
}
int HLPP(){
if(!bfs()) return 0;
memset(gap,0,sizeof(gap));
memset(flow,0,sizeof(flow));
memset(vis,0,sizeof(vis));
d[s]=n;
for(int i=1;i<=n*m;++i){
if(d[i]!=-1) gap[d[i]]++;
}
Init();
while(!q.empty()){
int u=q.top().u;
q.pop();
vis[u]=0;
Push(u);
if(flow[u]){
gap[d[u]]--;
if(!gap[d[u]]) Gap(u);
Relabel(u);
gap[d[u]]++;
vis[u]=1;
q.push((Node){u,d[u]});
}
}
return flow[t];//最终推到汇点的就是最大流
}
int main(){
scanf("%d%d",&n,&m);s=n*m+1,t=n*m+2;
for(int i=1;i<=n;++i){
for(int j=1,w;j<=m;++j){
bool ok=0;
scanf("%d",&w);
sum+=w;//总和
int k=getnum(i,j);
//构造二分图
if((i+j)%2) add(s,k,w),ok=1;//一个集连向源点
else add(k,t,w);//另一个集连向汇点
if(ok){
for(int q=0;q<4;++q){
int ex=i+dx[q],ey=j+dy[q];
if(check(ex,ey)){
int p=getnum(ex,ey);
if((ex+ey)%2) swap(k,p);
add(k,p,INF);//相邻点连边,注意顺序啊!
}
}
}
}
}
int ans=sum-HLPP();//我使用的是HLPP预留推进来做这个题的,没学过的请右转板子题P4722
printf("%d\n",ans);
return 0;
}
初学HLPP,板子不好看,请各位巨佬神犇见谅。