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\) 的边。
在该图上跑最小割即可。
该算法的正确性证明可以参见洛谷用户 @longlongzhu123 的题解,这篇题解给出了详细的证明过程。
那么最后最大权闭合子图的点权和就是所有 \(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;
}