P2805 [NOI2009] 植物大戰殭屍 題解
這道題是一道好題目,考察了建模能力。
但是因為資料過水導致建模建錯都能有 80 分
首先我們需要了解一個概念:最大權閉合子圖。
什麼是最大權閉合子圖?
對於一張有向圖 \(G=<V,E>\),我們從中選出一些點,如果這些點滿足以下條件,就稱這些點組成的圖為閉合子圖:
- 對於每一個被選出來的點,其在圖 \(G\) 中能夠到達的點都已經被選出來了。
比如說下面這張圖,\((a,b,c,d),(b,c,d)\) 是閉合子圖,但是 \((a,c,d)\) 不是,因為 \(a\) 能到 \(b\),但是 \(b\) 並沒有被選出來。
那麼最大權閉合子圖,就是所有閉合子圖中點權最大的閉合子圖。
所以最大權閉合子圖要怎麼求呢?採用最小割模型。
設點 \(i\) 的權值為 \(val_i\),\(s,t\) 是超源超匯。
- 對於每一個點 \(u\):
- 如果 \(val_u>0\),那麼 \(s\) 向 \(u\) 連一條流量為 \(val_u\) 的邊。
- 如果 \(val_u<0\),那麼 \(u\) 向 \(t\) 連一條流量為 \(-val_u\) 的邊。
- 如果 \(val_u=0\),上述兩種連邊方式隨意選擇一種即可。
- 對於原圖中存在的每條邊 \(u \to v\),從 \(u\) 向 \(v\) 連一條流量為 \(INF\) 的邊。
在該圖上跑最小割即可。
該演算法的正確性證明可以參見洛谷使用者 @
那麼最後最大權閉合子圖的點權和就是所有 \(val_i>0\) 的和減去最小割。
那麼最大權閉合子圖跟這道題有什麼關係呢?
- 規定 \((i,j)\) 表示第 \(i\) 行第 \(j\) 列的植物。
分析題意可以發現,要吃掉一株植物,當且僅當這株植物右邊的所有植物都被吃掉並且保護它的植物也被吃掉才行。
而我們需要從中選出點權和最大的植物們吃掉,這剛好與最大權閉合子圖吻合。
因此我們的建模方式出來了:
- 對於 \((u,v)\),向 \((u,v+1)\) 連邊,流量為 \(INF\),其中 \(1 \leq u \leq n,1 \leq v < m\)
- 如果 \((u,v)\) 保護著 \((x,y)\),那麼 \((x,y)\) 向 \((u,v)\) 連邊,流量為 \(INF\)。
- 對於每一個點 \((u,v)\),按照其點權的正負,從超源連邊/向超匯連邊,流量為 \(|val_{(u,v)}|\)。
為什麼是 \((u,v) \to (u,v+1)\) 而不是 \((u,v+1) \to (u,v)\)?因為如果要到 \((u,v)\) 就必須到 \((u,v+1)\)。
其實就是如果 \(x\) 保護 \(y\) 那麼 \(y\) 向 \(x\) 連邊。
在建圖完畢後,求出最大權閉合子圖即可。
但是如果你看到這裡就已經開始寫程式碼了,那麼你會發現你連樣例都過不去。
需要注意的是,如果兩個植物互相保護,那麼這兩個植物是需要踢出我們建的圖的,因為我們根本不可能吃掉這兩個植物。
同理,成環的植物我們吃不掉,被環上的點保護我們也吃不掉。
比如幾個無 CD 玉米加農炮互相對準對方不斷開炮
因此我們首先需要一遍拓撲排序去除這些點。
Code:
/*
========= Plozia =========
Author:Plozia
Problem:P2805 [NOI2009] 植物大戰殭屍
Date:2021/6/1
========= Plozia =========
*/
#include <bits/stdc++.h>
using std::queue;
typedef long long LL;
const int MAXM = 1000000 + 10, MAXN = 10000 + 10, INF = 0x7f7f7f7f;
int n, m, val[MAXN], cnt_Edge = 1, Head[MAXN], cnt_tp = 1, tp_Head[MAXN], cnt[MAXN], s, t, cur[MAXN];
int dep[MAXN], gap[MAXN];
struct node { int to, val, Next; } tp[MAXM], Edge[MAXM];
bool book[MAXN];
//超源:n * m + 1, 超匯:n * m + 2
int Read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
void add_Edge(int x, int y, int z) { ++cnt_Edge; Edge[cnt_Edge] = (node){y, z, Head[x]}; Head[x] = cnt_Edge; }
void tp_add(int x, int y, int z) { ++cnt_tp; tp[cnt_tp] = (node){y, z, tp_Head[x]}; tp_Head[x] = cnt_tp; }
int Change(int x, int y) { return (x - 1) * m + y; }
void Top_sort()
{
queue <int> q;
for (int i = 1; i <= n * m; ++i)
if (cnt[i] == 0) { q.push(i); book[i] = 1; }
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = tp_Head[x]; i; i = tp[i].Next)
{
int u = tp[i].to; --cnt[u];
if (cnt[u] == 0) { q.push(u); book[u] = 1; }
}
}
}
void bfs()
{
queue <int> q;
memset(dep, -1, sizeof(dep));
q.push(t); dep[t] = 0; ++gap[0];
while (!q.empty())
{
int x = q.front(); q.pop();
for (int i = Head[x]; i; i = Edge[i].Next)
{
int u = Edge[i].to;
if (dep[u] != -1) continue ;
dep[u] = dep[x] + 1; ++gap[dep[u]]; q.push(u);
}
}
}
int dfs(int now, int Flow)
{
if (now == t) return Flow;
int used = 0;
for (int i = cur[now]; i; i = Edge[i].Next)
{
cur[now] = i; int u = Edge[i].to;
if (Edge[i].val && dep[now] == dep[u] + 1)
{
int Minn = dfs(u, Min(Flow - used, Edge[i].val));
if (Minn)
{
Edge[i].val -= Minn; Edge[i ^ 1].val += Minn; used += Minn;
if (used == Flow) return used;
}
}
}
--gap[dep[now]];
if (gap[dep[now]] == 0) dep[s] = n * m + 3;
++dep[now]; ++gap[dep[now]];
return used;
}
int ISAP()
{
int ans = 0; bfs();
while (dep[s] < n * m + 2) { for (int i = 1; i <= n * m + 5; ++i) cur[i] = Head[i]; ans += dfs(s, INF); }
return ans;
}
int main()
{
n = Read(), m = Read(); s = n * m + 1, t = n * m + 2;
int sum = 0;
for (int i = 1; i <= n * m; ++i)
{
val[i] = Read();
int w = Read();
while (w--)
{
int x = Read() + 1, y = Read() + 1;
tp_add(i, Change(x, y), 0); ++cnt[Change(x, y)];
}
}
for (int i = 1; i <= n; ++i)
for (int j = m; j > 1; --j)
{ tp_add(Change(i, j), Change(i, j - 1), 0); ++cnt[Change(i, j - 1)]; }
Top_sort();
for (int i = 1; i <= n * m; ++i)
{
if (val[i] > 0 && book[i]) { add_Edge(s, i, val[i]); add_Edge(i, s, 0); }
else if (book[i]) { add_Edge(i, t, -val[i]); add_Edge(t, i, 0); }
}
for (int i = 1; i <= n * m; ++i)
if (book[i] && val[i] > 0) sum += val[i];
for (int i = 1; i <= n * m; ++i)
{
if (book[i] == 0) continue ;
for (int j = tp_Head[i]; j; j = tp[j].Next)
{
int u = tp[j].to;
if (book[u] == 0) continue ;
add_Edge(u, i, INF); add_Edge(i, u, 0);
}
}
printf("%d\n", sum - ISAP());
return 0;
}