方格取數問題(網路流24題之一)
方格取數
題目描述
在一個有 m*n 個方格的棋盤中,每個方格中有一個正整數。現要從方格中取數,使任意 2 個數所在方格沒有公共邊,且取出的數的總和最大。試設計一個滿足要求的取數演算法。對於給定的方格棋盤,按照取數要求程式設計找出總和最大的數。
輸入輸出格式
輸入格式:
第 1 行有 2 個正整數 m 和 n,分別表示棋盤的行數和列數。接下來的 m 行,每行有 n 個正整數,表示棋盤方格中的數。
輸出格式:
程式執行結束時,將取數的最大總和輸出
這道題的題意還是比較好理解的,就是不能取相鄰的兩個數,使所有取出來的數的總和最大。。
嗯,先不要急著想搜尋、暴力或貪心(雖然有可能可以過,如果可以,歡迎指教),這題可是出自網路流24題的呀。
所以肯定是用網路流 我們也可以用網路流來做。
首先,也是最關鍵的,這題如何建圖?
我們可以這樣想:
把格子染成黑白兩色,形如下圖: 有點醜。。好吧,不止一點
其實黑白兩色也可以反過來,不過這不是重點。
我們把每個格標個號:
然後我們可以把格子抽象成一個點,然後分開:
最後就是連邊:
左邊那個藍藍的是源點,然後那個白色的大圈是匯點。
從源點連向黑點的邊的權值(容量)是那個黑點的值,然後從白點連向匯點的邊也是權值。
最後中間那一大坨是。。。。嗯,邊的兩邊的點時候不能同時選的(因為有公共邊),邊權是INF(儘量大)。
我們要求的是什麼呢,沒錯,選出來的所有點的權值最大…… 這TMD和這圖有什麼關係啊?
別心急,這題應該是要求最大點權獨立集(注意:是點權),就等於所有點的值減去最小點權覆蓋集,就是減去最小割(最大流),然後我們就可以開心地寫程式碼了。
可能有些同學並不理解最後一句話是什麼意思(其實一開始我也不理解)。
其實是這樣的,我們相當與把點權轉換為了邊權,我們的目的是斷開一些邊,使得沒有路徑從源點到達匯點(為了滿足題目的限制條件嗎),然後我們要使斷開的邊的權值之和最小(斷開的邊就相當於是不選那個點,就是剩下的邊權之和最大->這就是我們要求的答案嘛),乍一看,這不就是最小割嗎?因為最小割=最大流,所以我們可以跑一遍最大流,然後用總的邊權減去最大流就是我們的答案了,是不是很棒棒。
總結:做完這題,我對網路流的理解又加深了一層。我們要巧妙的利用網路流的建模技巧(這也是網路流的精髓),把問題轉換為可以用網路流解決的問題,要多刷題,刷好題,才能掌握技巧。
上程式碼:
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int MAXN=1000005;
const int MAXM=1000005;
int d[MAXN],n,m,p[MAXN],eid,S,T,x[MAXN],y[MAXN],z[MAXN],a[105][105],sz,sum;
struct A{ //灰常正常的最大流(Dinic)
int v,c,next;
}e[MAXM];
void init(){
memset(p,-1,sizeof(p));
eid=0;
}
void add(int u,int v,int c){
e[eid].v=v;
e[eid].c=c;
e[eid].next=p[u];
p[u]=eid++;
}
void insert(int u,int v,int c){
add(u,v,c);
add(v,u,0);
}
int bfs(){
memset(d,-1,sizeof(d));
queue<int>q;
d[S]=0;
q.push(S);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=p[u];i!=-1;i=e[i].next){
int v=e[i].v;
if(e[i].c>0&&d[v]==-1){
d[v]=d[u]+1;
q.push(v);
}
}
}
return (d[T]!=-1);
}
int dfs(int u,int flow){
if(u==T) return flow;
int ret=0;
for(int i=p[u];i!=-1;i=e[i].next){
int v=e[i].v;
if(e[i].c>0&&d[v]==d[u]+1){
int tmp=dfs(v,min(flow,e[i].c));
e[i].c-=tmp;
e[i^1].c+=tmp;
flow-=tmp;
ret+=tmp;
if(!flow) break;
}
}
if(!ret) d[u]=-1;
return ret;
}
int Dinic(){
int ret=0;
while(bfs()){
ret+=dfs(S,INF);
}
return ret;
}
int main(){ //以下開始碼風突變(中了yjq的膜法)
init();
scanf("%d%d", &m, &n);
S=0;T=n*m+1;//建立源點和匯點
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
scanf("%d", &a[i][j]);
sum += a[i][j];
sz ++;
if((i + j) % 2){
insert(S, sz, a[i][j]);//連向源點
if(j < n) insert(sz, sz + 1, INF);//把有限制條件的連起來,邊權注意要儘量大
if(j > 1) insert(sz, sz - 1, INF);
if(i < m) insert(sz, sz + n, INF);
if(i > 1) insert(sz, sz - n, INF);
} else {
insert(sz,T,a[i][j]);//連向匯點
}
}
}
printf("%d",sum - Dinic());//總的邊權 - 最大流(最小割)
return 0;
}