P3520 [POI2011] SMI-Garbage

\(P3520\) \([POI2011]\) \(SMI-Garbage\)

\(LOJ\) \(2162\) 垃圾运输 \(Garbage\)
\(LOJ\)果然是好东西,所有数据完整提供下载,真是太人性化,洛谷太垃圾了!

一、题目描述

有一个可以看成 无向图 的城市,上面有 \(n\) 个点和 \(m\) 条边。

每一天,有若干辆垃圾车按照 环形 来跑一圈。并且,对于一辆垃圾车, 除了起点以外不能跑两次。

一条路有 \(2\) 种状态:清洁的(用 0 表示)或不清洁的(用 1 表示)。每次垃圾车经过,都会改变这条路的状态。

因为有些路上的人不交垃圾清理费,所以市长想要那些路变得不清洁;除此之外的路要清洁。那么,如何安排垃圾车,才能使得市长目的达到呢?

二、输入格式

输入的第一行包含两个空格分隔的正整数 \(n\)\(m\) \(( 1 \leqslant n \leqslant 100000,1 \leqslant m \leqslant 1000000)\),表示图的点数和边数。

接下来 \(m\) 行,每行包含四个空格分隔的正整数 $a,b,s,t $( $1 \leqslant a \leqslant b \leqslant n $ , \(s,t \in \lbrace0,1\rbrace\) ) ,表示一条联结结点 \(a\)\(b\) 的边,初始颜色和目标颜色分别为 \(s\)\(t\)

你可以认为若存在合法方案,则存在一个卡车经过总边数不超过 \(5m\) 的方案。

对于 \(60\%\) 分数的数据,有 $ m \leqslant 10000$。

三、输出格式

如果不存在合法方案,输出一行 NIE(波兰语「否」);

否则按下列格式输出任意一种方案:

  • 第一行包含一个整数 \(k\),表示卡车行驶环路的总数;
  • 接下来 \(k\) 行,每行描述一条环路:
    • 首先是一个正整数 \(k_i\) 表示环路经过的边数,后接一个空格;
    • 接下来 $ k_i + 1 $ 个空格分隔的整数,依次表示环路上结点的编号。

样例输入

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

样例输出

2
3 1 3 2 1
3 4 6 5 4

样例图示

二、题目解析

知识点:欧拉回路+异或+简单环

结合一下样例输出,明白了:

  • 因为跑一趟就会让状态变成相反的,所以,如果前后一样的,其实没必要跑,或者要跑偶数次。如果前后不一样的,就需要跑一次或者跑奇数次。

    • 如果原来是\(1\),后来是\(1\),或者,原来是\(0\),后来是\(0\),也就是红色虚线,只能走偶数数次。
    • 如果原来是\(1\),后来是\(0\),或者,原来是\(0\),后来是\(1\),也就是黑色实线,只能走奇数次。
  • 题目似乎在让我们找 简单环,两个输出示例,一个是上面的黑色线组成的环,另一个是下面黑色线组成的环。

  • 什么情况下存在合法方案,什么情况下不存在合法方案呢?
    答:在以前后不一致的边抽出来建图(黑色线),然后判断是不是每个点的度数都是偶数,就知道是不是每个点都在欧拉图中,如果有不存欧拉图中的,就是无法完成任务。

标准正确\(dfs\)答案

#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没有访问过,并且,是数据中出现过的点,比如1,2,5,6出现,3,4不参加讨论问题
            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;
}

三、代码解析

先给出一组测试数据:

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

我将按代码的执行流程来模拟一遍,可能挺长,耐心看完:

  • \(1\)号点出发,因为\(1-4\)\(1-7\)先给出,但由于 链式前向星是头插法,所以插入的在队头,所以先走\(7\),也就是现在\(1->7\)的箭头方向

  • \(7->3\),由于\(3->5\)是先给出的数据,\(3->6\)是后给出的数据,所以产生两个分支,一个向\(6\)前进,另一个向\(5\)前进,(为啥不先跑\(3->2\)?是因为\(3-2\)先给出,头插法,所以先跑\(5\))

  • \(2\)准备走向\(3\)时,发现\(3\)已经在栈中了,也就是找到了一个简单环:将栈中元素准备弹出\(2,5\)

  • \(Q:\)为什么是\(2,5\)呢?\(3\)哪去了?因为\(3\)不能出栈!,原因很简单,这个点\(3\)除了在这个简单环以外,还在其它简单环中!如果我们把\(3\)也出了栈,后面的\(1,7,3,6,4,1\)中就没有了\(3\), 那个就不是完整的简单环了!

  • \(3\)不出栈,本简单环也不完整啊!是的,我们需要手动补上入口的\(3\)和出口的\(3\)

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\)号节点,执行了下面的代码
stk.push_back(v);     // 入栈
instk[v] = 1;         // 标识在栈内

\(1\)号节点放入栈中。

至此,回到了\(1\)号节点,此时,由于\(1\)已经没有了出边,因为边都被我们删除掉了,可不没有了出边,递归终止了,没有机会将栈中存储的简单环路径:\(7,3,6,4\)保存成简单环!也就是在\(dfs(1)\)这样的起点后面,需要手动出栈,一出到底即可!不出干净还不行,会影响后面的其它环了~

res[++rl].push_back(i);
while (stk.size()) {
    res[rl].push_back(stk.back());
    instk[stk.back()] = 0;
    stk.pop_back();
}            

四、并查集踩坑

我最开始错误的以为使用并查集一样可以完成任务,因为环嘛,当然是通过边相连的,如果用并查集维护,不就是可以找出哪些点通过边相连,而且,前面已经证明了存在欧拉回路,枚举每个家族的族长,以它为起点出发,不就可以找出所有环吗?

但事实证明我 错的离谱!原因是题目要求找出的是 简单环\(Simple \ Circle\)!比如下面的数据样例直接教你做人(参考下图中右侧部分)!我的答案就会把左边的那一大堆认为是一个环,可人家答案要求的是多个简单环,也就是:

3
3 3 5 2 3 
5 1 7 3 6 4 1 
3 9 13 14 9 

我的答案是这样滴:

2
7 6 4 1 7 3 2 5 3 6 
3 13 14 9 13 

这不是简单环,违背了原则:除起点外,其它点出现\(1\)
节点\(3\)出现了两次,节点\(6\)出现了两次,是标准错误答案。

结论

这种找简单环,只能是用\(dfs\),不能用并查集

标准错误并查集答案

#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;
}
posted @ 2023-08-07 08:50  糖豆爸爸  阅读(41)  评论(0编辑  收藏  举报
Live2D