【luogu P2764】【LOJ 6002】最小路径覆盖问题
最小路径覆盖问题
题目链接:luogu P2764 / LOJ 6002
题目大意
给你一个 DAG,要你找最少数量的路径,使得一个点只会被一个路径经过,也一定会有路径经过它。
路径的长度可以是 \(0\),即一个点也代表一个路径。
输出路径数量和每条路径的走法。
思路
首先你知道求路径数量就是直接拆点然后网络流就好了。
至于问什么,这里就简单说说:
大概是一开始所有点都是一个路径,然后你加你给出的路径就相当于可以把给出的路径相连的路径合并成一个路径。
然后路径又不能有公共点,那新加的边就不能有公共点,你就会发现它其实就是在进行匹配。
那你最多匹配了 \(x\) 对,原来有 \(n\) 个点,那匹配一次两个路径边一个,减少一,那最小路径覆盖就是 \(n-x\) 咯。(\(x\) 就是我们二分图的最大匹配)
那建图就是把一个点 \(i\) 拆成 \(ix,iy\) 两个点,分别在二分图的左边右边。
然后 \(i\) 连向 \(j\) 就相当于二分图中 \(ix\rightarrow jy\) 这条边。
但其实这道题最烦的应该是如何输出每条路径。
考虑到 \(n\) 很小,\(n^2\) 都可以过,我们考虑暴力跳。
那你网络流要流的时候就记录从这个点开始留到哪个点(其实就是这个点跟右边的哪个点匹配),然后再记录右边的那些点是否能被留到。
如果一个右边的点不能被留到,就说明它应该是一个起点,就从它左边对于的点开始流,流到右边,再跑到它对应左边的点。一直这样跳下去,知道你流不到点或留到的不是右边的点,中途我们跳到的带点对应原来的点就是我们要顺序输出的。
然后就好了。
不过这题的 SPJ 比较神奇,你搞的时候一定要注意跑到它对应左边的点要判断是否是右边的点再太跳,而且这个判断一定要比较严谨。不然的话不开 O2 可以过,开了 O2 就 RE+TLE,LOJ 可以选开不开,但洛谷的这道题是一定会开的。
代码
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#define INF 1000000
using namespace std;
struct node {
int x, to, nxt, op;
}e[200001];
int n, m, x, y, S, T, ans, dis[20001];
int le[20001], lee[20001], KK, gogo[20001];
bool ru[20001];
queue <int> q;
void add(int x, int y, int z) {
e[++KK] = (node){z, y, le[x], KK + 1}; le[x] = KK;
e[++KK] = (node){0, x, le[y], KK - 1}; le[y] = KK;
}
int get_num(int x, int op) {
return op * n + x;
}
//网络流 dinic 算法
bool bfs() {
for (int i = 1; i <= T; i++) {
dis[i] = -1;
lee[i] = le[i];
}
while (!q.empty()) q.pop();
q.push(S);
dis[S] = 0;
while (!q.empty()) {
int now = q.front();
q.pop();
for (int i = le[now]; i; i = e[i].nxt)
if (e[i].x > 0 && dis[e[i].to] == -1) {
dis[e[i].to] = dis[now] + 1;
q.push(e[i].to);
}
}
return dis[T] != -1;
}
int dfs(int now, int sum) {
if (now == T) return sum;
int go = 0;
for (int &i = lee[now]; i; i = e[i].nxt)
if (e[i].x > 0 && dis[e[i].to] == dis[now] + 1) {
int this_go = dfs(e[i].to, min(sum - go, e[i].x));
if (this_go) {
gogo[now] = e[i].to;//记录流向和对于流向边是否有入度
if (e[i].to > n && e[i].to <= 2 * n) ru[e[i].to - n] = 1;
//一定要加判断条件,不加的话不开 O2 可以过开了就 RE+TLE(luogu 自带 O2)
e[i].x -= this_go;
e[e[i].op].x += this_go;
go += this_go;
if (go == sum) return go;
}
}
if (!go) dis[now] = -1;
return go;
}
void dinic() {
while (bfs())
ans -= dfs(S, INF);
}
int read() {
int re = 0, zf = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') zf = -zf;
c = getchar();
}
while (c >= '0' && c <= '9') {
re = (re << 3) + (re << 1) + c - '0';
c = getchar();
}
return re * zf;
}
int main() {
// freopen("read.txt", "r", stdin);
n = read();
m = read();
for (int i = 1; i <= m; i++) {
x = read();
y = read();
add(get_num(x, 0), get_num(y, 1), 1);
}
S = 2 * n + 1;
T = 2 * n + 2;
for (int i = 1; i <= n; i++) {
add(S, get_num(i, 0), 1);
add(get_num(i, 1), T, 1);
}
ans = n;
dinic();
for (int i = 1; i <= n; i++)//输出方案
if (!ru[i]) {//找到没有流边入度的点,就说明从这里开始
printf("%d", i);
int now = i;
while (gogo[now] > n && gogo[now] <= 2 * n) {//不停根据你求出的走的方向走
now = gogo[now] - n;
printf(" %d", now);
}
printf("\n");
}
printf("%d\n", ans);
return 0;
}