You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

369 lines
14 KiB

2 years ago
##[$P3520$ $[POI2011]$ $SMI-Garbage$](https://www.luogu.com.cn/problem/P3520)
> [$LOJ$ $2162$ 垃圾运输 $Garbage$](https://loj.ac/s/1857935)
>**注**$LOJ$果然是好东西,所有数据完整提供下载,真是太人性化,洛谷太垃圾了!
### 一、题目描述
有一个可以看成 **无向图** 的城市,上面有 $n$ 个点和 $m$ 条边。
每一天,有若干辆垃圾车按照 **环形** 来跑一圈。并且,**对于一辆垃圾车** 除了起点以外不能跑两次。
一条路有 $2$ 种状态:清洁的(用 `0` 表示)或不清洁的(用 `1` 表示)。每次垃圾车经过,都会改变这条路的状态。
因为有些路上的人不交垃圾清理费,所以市长想要那些路变得不清洁;除此之外的路要清洁。那么,如何安排垃圾车,才能使得市长目的达到呢?
### 二、输入格式
输入的第一行包含两个空格分隔的正整数 $n$ 和 $m$ $ 1 \leqslant n \leqslant 1000001 \leqslant m \leqslant 1000000$,表示图的点数和边数。
接下来 $m$ 行,每行包含四个空格分隔的正整数 $a,b,s,t $ $1 \leqslant a \leqslant b \leqslant n $ , $st \in \lbrace0,1\rbrace$ ,表示一条联结结点 $a$ 与 $b$ 的边,初始颜色和目标颜色分别为 $s$ 与 $t$ 。
你可以认为若存在合法方案,则存在一个卡车经过总边数不超过 $5m$ 的方案。
对于 $60\%$ 分数的数据,有 $ m \leqslant 10000$。
### 三、输出格式
如果不存在合法方案,输出一行 `NIE`(波兰语「否」);
否则按下列格式输出任意一种方案:
- 第一行包含一个整数 $k$,表示卡车行驶环路的总数;
- 接下来 $k$ 行,每行描述一条环路:
- 首先是一个正整数 $k_i$ 表示环路经过的边数,后接一个空格;
- 接下来 $ k_i + 1 $ 个空格分隔的整数,依次表示环路上结点的编号。
**样例输入**
```cpp {.line-numbers}
6 8
1 2 0 1
2 3 1 0
1 3 0 1
2 4 0 0
3 5 1 1
4 5 0 1
5 6 0 1
4 6 0 1
```
**样例输出**
```cpp {.line-numbers}
2
3 1 3 2 1
3 4 6 5 4
```
**样例图示**
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230807080212.png)
### 二、题目解析
#### 知识点:欧拉回路+异或+简单环
结合一下样例输出,明白了:
- 因为跑一趟就会让状态变成相反的,所以,如果前后一样的,其实没必要跑,或者要跑偶数次。如果前后不一样的,就需要跑一次或者跑奇数次。
- 如果原来是$1$,后来是$1$,或者,原来是$0$,后来是$0$,也就是红色虚线,只能走偶数数次。
- 如果原来是$1$,后来是$0$,或者,原来是$0$,后来是$1$,也就是黑色实线,只能走奇数次。
- 题目似乎在让我们找 **简单环**,两个输出示例,一个是上面的黑色线组成的环,另一个是下面黑色线组成的环。
- 什么情况下存在合法方案,什么情况下不存在合法方案呢?
答:在以前后不一致的边抽出来建图(**黑色线**),然后判断是不是每个点的度数都是偶数,就知道是不是每个点都在欧拉图中,如果有不存欧拉图中的,就是无法完成任务。
#### 标准正确$dfs$答案
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 2e6 + 10;
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int d[N]; // 度
vector<int> res[N]; // 每个环中有哪些节点
int rl; // 环的数量游标
int vist[N]; // 某个点是不是访问过
int st[M]; // 边是不是访问过,用于无向图的成对变换优化
vector<int> stk; // 找不用的栈
int instk[N]; // 节点i是不是在栈内
void dfs(int u) {
// u节点已访问过之所以需要vist[u]这样的状态数组,本质上是因为本题是一张图中多组欧拉回路,
// 没跑过的点才需要当做起点跑欧拉回路,需要记录哪个跑过哪个没跑过
vist[u] = 1;
for (int i = h[u]; ~i; i = h[u]) { // 枚举u节点的每个出边i
h[u] = ne[i]; // 删边优化
if (st[i]) continue; // 此边访问过,不用再讨论,其实,这是在处理成对变换的另一边
st[i] = st[i ^ 1] = 1; // 无向图,成对标识已被访问过
int v = e[i]; // 节点u通过边i指向点v
dfs(v); // 深搜v=e[i]这个点
if (instk[v]) { // 如果v点在栈中说明找到了行进路线中的环
res[++rl].push_back(v); // v记录到rl号环中
while (stk.back() != v) { // 一直把栈顶元素弹出直到找出首次进入的那个v也就是通过栈找环
res[rl].push_back(stk.back()); // 将环中的节点记录到res[rl]这个结果集中去
instk[stk.back()] = 0; // 标识栈顶元素已出栈
stk.pop_back(); // 栈顶元素出栈
}
res[rl].push_back(v); // 由于上面使用的是stk.back()!=v,这样是为了保持v还在栈中让这个点重复命使用可以参考图2的点3
} else { // 如果不在栈内
stk.push_back(v); // 入栈
instk[v] = 1; // 标识在栈内
}
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("P3520.in", "r", stdin);
#endif
memset(h, -1, sizeof h); // 初始化链式前向星
int n, m;
scanf("%d %d", &n, &m); // n个顶点m条边
int a, b, s, t;
while (m--) {
scanf("%d %d %d %d", &a, &b, &s, &t); // a到b有一条边颜色初始值是s,目标终止值是t
if (s ^ t) { // 如果s与t不一样,建边。欧拉图需要每边只走一次
add(a, b), add(b, a); // 双向建边,因为可以正着走也可以反着走但正反边只能走1次
d[a]++, d[b]++; // 维护度
}
}
// 检查是否某个点的度是奇数,这样没有欧拉回路
for (int i = 1; i <= n; i++)
if (d[i] % 2) {
puts("NIE");
exit(0);
}
// 遍历每个节点
for (int i = 1; i <= n; i++)
if (!vist[i] && d[i]) { // 如果节点i没有访问过并且是数据中出现过的点比如1256出现34不参加讨论问题
dfs(i); // 对节点i进行深搜找简单环
/*简单环分两种情况:
① 出发点在内的简单环
② 在行进路线上出现的简单环
这两种情况处理办法不一样,需要分别对待,下面就是情况①
由于上面已经通过一笔画的定理排除掉了非欧拉回路的可能,现在的图肯定是欧拉回路
那么在dfs过程中我们可以把路径上的环处理掉那么一定有从起点出发的环还在栈中需要单独处理
*/
res[++rl].push_back(i);
while (stk.size()) {
res[rl].push_back(stk.back());
instk[stk.back()] = 0;
stk.pop_back();
}
}
// 输出环的数量
printf("%d\n", rl);
// 遍历每个环
for (int i = 1; i <= rl; i++) {
printf("%d ", res[i].size() - 1); // 输出环中节点个数这里为什么要减1呢因为是环嘛首尾是一样的加入了两次
for (auto x : res[i]) printf("%d ", x);
puts("");
}
return 0;
}
```
### 三、代码解析
先给出一组测试数据:
```cpp {.line-numbers}
20 23
7 3 0 1
3 2 1 0
5 6 1 1
6 4 1 0
4 1 0 1
2 5 0 1
7 4 0 0
1 7 0 1
7 6 1 1
3 5 1 0
3 6 0 1
17 8 0 0
8 16 1 1
19 18 0 0
18 1 1 1
18 20 1 1
20 19 1 1
9 10 1 1
10 13 0 0
13 14 0 1
14 9 0 1
9 15 0 0
9 13 0 1
```
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230810114052.png)
我将按代码的执行流程来模拟一遍,可能挺长,耐心看完:
- 从$1$号点出发,因为$1-4$比$1-7$先给出,但由于 **链式前向星是头插法**,所以插入的在队头,所以先走$7$,也就是现在$1->7$的箭头方向
- $7->3$,由于$3->5$是先给出的数据,$3->6$是后给出的数据,所以产生两个分支,一个向$6$前进,另一个向$5$前进,(为啥不先跑$3->2$?是因为$3-2$先给出,头插法,所以先跑$5$)
- $2$准备走向$3$时,发现$3$已经在栈中了,也就是找到了一个简单环:将栈中元素准备弹出$25$
- $Q:$为什么是$2,5$呢?$3$哪去了?**因为$3$不能出栈!**,原因很简单,这个点$3$除了在这个简单环以外,还在其它简单环中!如果我们把$3$也出了栈,后面的$1,7,3,6,4,1$中就没有了$3$, 那个就不是完整的简单环了!
- 那$3$不出栈,本简单环也不完整啊!是的,我们需要手动补上入口的$3$和出口的$3$
```cpp {.line-numbers}
res[++rl].push_back(v); //入口的3
while (stk.back() != v) { //将栈中不包含3的节点记录到路径中
res[rl].push_back(stk.back());
instk[stk.back()] = 0;
stk.pop_back();
}
res[rl].push_back(v); //出口的3
```
- 继续行进,$3->6->4->1$,
然后$4$号节点,执行了下面的代码
```cpp {.line-numbers}
stk.push_back(v); // 入栈
instk[v] = 1; // 标识在栈内
```
将$1$号节点放入栈中。
至此,回到了$1$号节点,此时,由于$1$已经没有了出边,因为边都被我们删除掉了,可不没有了出边,递归终止了,没有机会将栈中存储的简单环路径:$7,3,6,4$保存成简单环!也就是在$dfs(1)$这样的起点后面,需要手动出栈,一出到底即可!不出干净还不行,会影响后面的其它环了~
```cpp {.line-numbers}
res[++rl].push_back(i);
while (stk.size()) {
res[rl].push_back(stk.back());
instk[stk.back()] = 0;
stk.pop_back();
}
```
### 四、并查集踩坑
我最开始错误的以为使用并查集一样可以完成任务,因为环嘛,当然是通过边相连的,如果用并查集维护,不就是可以找出哪些点通过边相连,而且,前面已经证明了存在欧拉回路,枚举每个家族的族长,以它为起点出发,不就可以找出所有环吗?
但事实证明我 **错的离谱**!原因是题目要求找出的是 **简单环**$Simple \ Circle$!比如下面的数据样例直接教你做人(参考下图中右侧部分)!我的答案就会把左边的那一大堆认为是一个环,可人家答案要求的是多个简单环,也就是:
```cpp {.line-numbers}
3
3 3 5 2 3
5 1 7 3 6 4 1
3 9 13 14 9
```
我的答案是这样滴:
```cpp {.line-numbers}
2
7 6 4 1 7 3 2 5 3 6
3 13 14 9 13
```
这不是简单环,违背了原则:**除起点外,其它点出现$1$次**。
节点$3$出现了两次,节点$6$出现了两次,是标准错误答案。
#### 结论
<font color='red' size=4><b>这种找简单环,只能是用$dfs$,不能用并查集</b></font>
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230810085346.png)
#### 标准错误并查集答案
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 2e6 + 10;
// 标准错误答案@
// #2162. 「POI2011 R2 Day1」垃圾运输 Garbage
// https://loj.ac/s/1857935
// 并查集
int p[N], sz[N];
int find(int x) {
if (x == p[x]) return x;
return p[x] = find(p[x]);
}
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int d[N];
vector<int> res[N];
int rl;
int st[M];
void dfs(int u) {
for (int i = h[u]; ~i; i = h[u]) {
h[u] = ne[i];
if (st[i]) continue;
st[i] = st[i ^ 1] = 1;
int v = e[i];
dfs(v);
cout << u << " ";
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("P3520.in", "r", stdin);
#endif
memset(h, -1, sizeof h);
int n, m;
scanf("%d %d", &n, &m);
// 初始化并查集
for (int i = 1; i <= n; i++) p[i] = i, sz[i] = 1;
int a, b, s, t;
while (m--) {
scanf("%d %d %d %d", &a, &b, &s, &t);
if (s ^ t) {
add(a, b), add(b, a);
d[a]++, d[b]++;
// 并查集
int pa = find(a), pb = find(b);
if (pa != pb) {
p[pb] = pa;
sz[pa] += sz[pb]; // 维护家庭人员数量
}
}
}
for (int i = 1; i <= n; i++)
if (d[i] % 2) {
puts("NIE");
exit(0);
}
// 肯定是存在欧拉回路,现在是几个环呢?可以用并查集提前处理出来
// 从头到尾,看一下有多少个家庭,就是有多少个环
int cnt = 0;
for (int i = 1; i <= n; i++)
if (d[i] && i == p[i]) cnt++;
cout << cnt << endl;
for (int i = 1; i <= n; i++)
if (d[i] && i == p[i]) {
cout << sz[i] << " " << i << " ";
dfs(i);
cout << endl;
}
return 0;
}
```