P3520 [POI2011] SMI-Garbage

蒟蒻豆进阶之路 / 2023-08-07 / 原文

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

题目描述

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

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

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

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

By @dengziyue

感谢 @cn:苏卿念 提供\(SPJ\)

输入格式

输入的第一行包含两个空格分隔的正整数 \(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 $ 个空格分隔的整数,依次表示环路上结点的编号。

样例 #1

样例输入 #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

样例输出 #1

2
3 1 3 2 1
3 4 6 5 4

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

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

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

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

#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];    // 是不是在栈内

/*
每次要求走过一个简单环上的边,即从某个点开始走过该点存在的联通图(原图可能不联通),
初始权值与最终权值相等的边走偶数次,不相等的走奇数次。

为什么用欧拉回路?
欧拉回路:存在一条从某个点开始,恰好不重不漏经过每条边一次(点可重复),最终回到该点。
欧拉图:存在欧拉回路的图。
欧拉图的判定:每个点的度数都为偶数。
通过以上定义和判定,可以考虑权值不相等的边通过找欧拉回路的方法来。因为依题意,权值相等的边必须走偶数次才能达到目标,
而欧拉图只要求走一次,和欧拉图不一致。
权值相等的边,因为每次都必须走偶数次,相当于拆成了两条边,这样实际上就使得该边对连接的两点度数加了+2,
根据欧拉回路的定义,删去这条边即对两点度数-2,还使得图成为欧拉图了(只用走剩下的边一次),就可以直接找欧拉回路了。
*/

// 在求欧拉回路的过程中,找环,记录下有几个环,都是啥节点
void dfs(int u) {
    // u节点已访问过,之所以需要vist[u]这样的状态数组,本质上是因为本题是一张图中多组欧拉回路,
    // 没跑过的点才需要当做起点跑欧拉回路,需要记录哪个跑过哪个没跑过
    // 所以说,对于标准模板的话,是没有这句的
    vist[u] = 1;

    for (int i = h[u]; ~i; i = ne[i]) { // 枚举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号环中
            // 这里无法用类似 int x=stk.back()的办法进行精简代码,那样就失去了stk.back()的动作,导致下面的代码失效
            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); // 同一个节点出现两次,记录两次,这才是环,题目要求输出这样的格式
            ++rl;                 // 环的数量+1
        } 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;                // n个顶点,m条边
    scanf("%d %d", &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);             // 双向建边
            ++d[a], ++d[b];                   // 维护度
        }
    }
    // 检查是否某个点的度是奇数,这样没有欧拉回路
    for (int i = 1; i <= n; i++)
        if (d[i] % 2) {
            puts("NIE");
            return 0;
        }

    // 遍历每个节点
    for (int i = 1; i <= n; i++)
        if (!vist[i]) {                            // 如果节点i没有访问过
            dfs(i);                                // 对节点i进行深搜
            if (instk[i]) {                        // 如果此节点i在栈中,表示找到了环。栈是本轮的概念,本轮跑圈中是不是重现节点i,这表示有环
                res[rl].push_back(i);              // 第rl个环中,增加i这个节点号
                while (stk.size()) {               // 利用循环,把栈中所有节点出栈,记录到结果数组中
                    res[rl].push_back(stk.back()); // 将栈中记录的节点,都增加到rl这个环中去
                    instk[stk.back()] = 0;         // 记录这个节点已经出栈
                    stk.pop_back();                // 节点出栈
                }
                ++rl; // 环数量+1
            }
        }
    // 输出环的数量
    printf("%d\n", rl);
    // 遍历每个环
    for (int i = 0; i < rl; i++) {
        printf("%d ", res[i].size() - 1); // 输出环中节点个数,这里为什么要减1呢?因为是环嘛,首尾是一样的,加入了两次
        for (auto x : res[i])
            printf("%d ", x);
        puts("");
    }
    return 0;
}