简单图论杂题

· · 算法·理论

::::info[前言]{open} 本文使用 deepseek+chatgpt(从未打过这么富裕的仗)润色。保证人类贡献远大于 AI 贡献。 ::::

Triangle City

注意到最优的解法需要经过图中的所有边至少一次。每个顶点的度数都是偶数,本题要求从 (1,1) 到 (n,n),所以需要的是欧拉路径,即要求恰好起点和终点度数为奇数,其余点度数为偶数。 ::::info[欧拉路径]{open} 欧拉路径是经过图中每条边恰好一次的路径。存在条件是恰好有两个顶点度数为奇数,这两个点就是起点和终点。\ 我们求欧拉路径一般使用 Hierholzer 算法。\ 下定义 E_u 为列表中每项记录邻边另一端点及该边编号。我们维护一个 p_u 表示每一个扫描位置 p_u 表示下一条待检查的边在 E_u 中的下标。我们从起点 s 开始,对当前顶点 u 执行以下操作:

const int N = 3e6 + 5; const int M = 3e6 + 5; const int INF = 1e18; typedef pair<int,int> pii; int n,u,v,w,ans,p[N],mp[M],cur[N]; vector<pii> G[N],g[N]; int dist[N],vis[N]; priority_queue<pii,vector<pii>,greater<pii> > q; void dijkstra(){ for(int i = 0;i <= (n - 1) (n + 1) + n;i ++) dist[i] = INF; dist[1] = 0; q.push({0,1}); while(!q.empty()){ int u = q.top().second; q.pop(); if(vis[u]) continue; vis[u] = 1; for(auto now : g[u]){ int v = now.first; int w = now.second; if( dist[v] > dist[u] + w){ p[v] = u; dist[v] = dist[u] + w; q.push({dist[v],v}); } } } } vector<int> Q; void Mark(){ u = (n-1)(n+1)+n; while(u != 1){ int prev = p[u]; for(auto now : G[u]){ int v = now.first; int id = now.second; if(v == prev){ mp[id] = 1; break; } } u = prev; } } void dfs(int u){ for(;cur[u] < G[u].size();cur[u] ++){ int v = G[u][cur[u]].first; int id = G[u][cur[u]].second; if(mp[id]) continue; mp[id] = 1; dfs(v); } Q.push_back(u); }

signed main(){ int T; scanf("%lld",&T); while(T --){ ans = 0; int ec = 0; scanf("%lld",&n); int mx = (n - 1) (n + 1) + n; for(int i = 0;i <= mx;i ++){ G[i].clear(); g[i].clear(); vis[i] = 0; p[i] = 0; cur[i] = 0; } for(int i = 0;i < M;i ++) mp[i] = 0; Q.clear(); while(!q.empty()) q.pop(); for(int i = 1;i < n;i ++){ for(int j = 1;j <= i;j ++){ u = (i - 1) (n + 1) + j; v = i (n + 1) + j; scanf("%lld",&w); ec ++; G[u].push_back({v,ec}); G[v].push_back({u,ec}); g[u].push_back({v,w}); g[v].push_back({u,w}); ans += w; } } for(int i = 1;i < n;i ++){ for(int j = 1;j <= i;j ++){ u = (i - 1) (n + 1) + j; v = i (n + 1) + j + 1; scanf("%lld",&w); ec ++; G[u].push_back({v,ec}); G[v].push_back({u,ec}); g[u].push_back({v,w}); g[v].push_back({u,w}); ans += w; } } for(int i = 1;i < n;i ++){ for(int j = 1;j <= i;j ++){ u = i (n + 1) + j; v = i (n + 1) + j + 1; scanf("%lld",&w); ec ++; G[u].push_back({v,ec}); G[v].push_back({u,ec}); g[u].push_back({v,w}); g[v].push_back({u,w}); ans += w; } } dijkstra(); ans -= dist[(n - 1) (n + 1) + n]; printf("%lld\n",ans); Mark(); dfs(1); printf("%lld\n",Q.size()); for(int i = Q.size() - 1;i >= 0;i --){ int tmp = Q[i]; int x = (tmp - 1) / (n + 1) + 1; int y = (tmp - 1) % (n + 1) + 1; printf("%lld %lld ",x,y); } puts(""); } return 0; }

::::
## [电报 / Telegraph](https://www.luogu.com.cn/problem/P14401)
这个题本质上是一个贪心。\
注意到整个图一开始的时候每一个节点的出度都为 $1$,但是入度不知道。我们的目的是构造最终要覆盖所有点的大环,那么我们需要最终每个点的入度恰好为 $1$ 且整个图联通。那么问题变为:
> 给定一张图,每一个节点的出度为 $1$,给定所有的 $n$ 条边形如 $i\to a_i$,可以改动一些边使得 $i\to j$,花费为 $c_i$。最终要求整个图是一个覆盖所有点的大环。

那么如果一开始的时候就构成了一个大环那么花费显然为 $0$。\
对于一个节点 $u$,我们维护 $w_u$ 表示已经处理的指向 $u$ 的点 $c$ 的最大值。由于最终 $u$ 只能保留一个入边,其它的全部删掉,所以保留 $w_u$ 应该是最优的。我们假设每一个点都要改变(事实上可以不变,待会再说),那么我们的花费就是 
$$Ans=\sum_{i=1}^n (c_i-w_i)$$
事实上,这样会导致一些点可以不做改动更优但是改了造成了无关的花费。我们拓扑排序之后,剩下的点都是环上的节点。我们对于每一个环上节点 $u$,那么我们改变 $u$ 的净收益是 $w_{a_u}-c_u$,因为 $a_u$ 可以选择在树子节点中保留一个。那么如果改变 $u$ 是赚的($w_{a_u}-c_u>0$)我们就可以节省下来这个点的花费。如果所有的都不赚,我们选择一个节点去断边,我们选择 $w_{a_u}-c_u$ 最大的 $u$。因为这样实际上 $c_u-w_{a_u}$ 是最小的。
::::success[P14401]
```c++
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 5;
const long long INF = -1e18;
int n,a[N],c[N],d[N],w[N],col[N],cnt,ans;
vector<int> v[N];
queue<int> q;

void dfs(int x){
    int cur = x;
    while(!col[cur]) {
        col[cur] = cnt;
        v[cnt].push_back(cur);
        cur = a[cur];
    }
}

void topu() {
    for(int i = 1;i <= n;i ++) if(!d[i]) q.push(i);
    while(!q.empty()) {
        int u = q.front();
        q.pop();

        ans += c[u] - w[u];
        w[a[u]] = max(w[a[u]],c[u]);

        d[a[u]] --;
        if(!d[a[u]]) q.push(a[u]);
    }
}

signed main(){
    scanf("%lld",&n);
    for(int i = 1;i <= n;i ++){
        scanf("%lld%lld",&a[i],&c[i]);
        d[a[i]] ++;
    }

    int t = 1;
    for(int i = 1;i <= n;i ++) {
        if(d[i] != 1) {
            t = 0;
            break; 
        }
    } 

    if(t) {
        int cur = 1;
        int cnt = 0;
        while(true) {
            cnt++;
            cur = a[cur];
            if(cur == 1) break;
        }
        if(cnt == n) {
            puts("0");
            return 0;
        }
    }

    topu();

    for(int i = 1;i <= n;i ++) {
        if(d[i] && !col[i]) {
            cnt ++;
            dfs(i); 
        }
    }

    for(int i = 1;i <= cnt;i ++) {
        int mx = INF;
        for(auto u : v[i]) {
            int vt = w[a[u]] - c[u];
            if(vt > 0) ans -= vt;
            mx = max(mx,vt); 
        }
        if(mx < 0) ans -= mx;
    }

    printf("%lld\n",ans);
    return 0; 
}

::::

Go Playing with Reimu

我们设 f_u=\{1,0\} 表示棋子位于节点 u 轮到先手时先手是否必胜,其中 f_u=1 表示先手必胜,f_u=0 表示先手必败。那么

但是我们发现这两个人会删掉叶子节点 x_i,时间复杂度是 O(nq) 的。我们观察到 f_u 只是依赖于是否存在必败态儿子。我们把儿子分为重儿子和轻儿子。那么我们记录 \text{cnt}_u 为节点 u 的轻儿子中满足 f_v=0 的数量。那么

由于重链区间的 DFS 序是连续的,将所有满足 \text{cnt}_u>0 的节点的 DFS 序存入 set 中。这样可以在 O(\log n) 时间内找到重链区间内第一个 \text{cnt}_u>0 的位置。我们注意到删掉一个节点可能会导致一条链的 f_u 的改变,所以我们向上传播修改。\ 时间复杂度是 O((n+q)\log^2 n) 的。 ::::success[P16411]

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 3e5 + 5;
int n,q,tim;
vector<int> G[N];
int fa[N],dep[N],siz[N],son[N],f[N],cnt[N];
int dfn[N],id[N],top[N],st[N][20];
int del[N];
set<int> S;

void dfs1(int u,int p){
    fa[u] = p;
    dep[u] = dep[p] + 1;
    siz[u] = 1;
    for(auto v : G[u]){
        if(v == p) continue;
        dfs1(v,u);
        siz[u] += siz[v];
        if(siz[v] >= siz[son[u]]) son[u] = v;
    }
}

void dfs2(int u,int gr){
    dfn[++ tim] = u;
    id[u] = tim;
    top[u] = gr;
    if(son[u]) dfs2(son[u],gr);
    for(auto v : G[u]){
        if(v == fa[u] || v == son[u]) continue;
        dfs2(v,v);
        if(!f[v]) cnt[u] ++;
    }
    if(cnt[u]) f[u] = 1;
    else f[u] = f[son[u]] ^ 1;
    st[u][0] = son[u];
    for(int i = 1;i <= 18;i ++) st[u][i] = st[st[u][i - 1]][i - 1];
}

int len(int u){
    int res = 0;
    for(int i = 18;i >= 0;i --)
        if(!del[st[u][i]]) {res += (1 << i);u = st[u][i];}
    return res;
}

int find(int l,int r){
    auto it = S.lower_bound(l);
    if(it == S.end() || *it > r) return 0;
    return *it - 1;
}

int query(int u){
    if(!u || del[u]) return 1;
    if(S.count(id[u])) return 1;
    int l = id[u];
    int r = id[u] + len(u);
    int pos = find(l,r);
    if(!pos) return (r - l) & 1;
    else return (pos - l) & 1;
}

void update(int u,int lst,int y){
    if(!u) return;
    if(y == 1){
        cnt[u] --;
        if(cnt[u] == 0){
            int tmp = query(top[u]);
            S.erase(id[u]);
            if(tmp != query(top[u])) update(fa[top[u]],top[u],tmp ^ 1);
        }
    } else {
        cnt[u] ++;
        if(cnt[u] == 1){
            int tmp = query(top[u]);
            S.insert(id[u]);
            if(tmp != query(top[u])) update(fa[top[u]],top[u],tmp ^ 1);
        }
    }
}

signed main(){
    scanf("%lld",&n);
    del[0] = 1;
    for(int i = 1;i < n;i ++){
        int u,v;
        scanf("%lld%lld",&u,&v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    f[0] = 1;
    dfs1(1,0);
    dfs2(1,1);
    for(int i = 1;i <= n;i ++) if(cnt[i]) S.insert(id[i]);
    if(f[1]) puts("Marisa");
    else puts("Reimu");
    scanf("%lld",&q);
    for(int i = 1;i <= q;i ++){
        int k;
        scanf("%lld",&k);
        int tmp = query(top[k]);
        del[k] = 1;
        int tmp2 = query(top[k]);
        if(tmp != tmp2) update(fa[top[k]],top[k],tmp ^ 1);
        if(query(1)) puts("Marisa");
        else puts("Reimu");
    }
    return 0;
}

::::

Menji, we miss you!

我们设 d_u 为节点 u 的深度。单独设 D=d_X。我们考虑二分去得到 D,我们每一次查询 \text{ask}(1,\text{mid}),最终 D=\text{mid}。\ 我们确定了 D 以后,所有深度恰好为 D 的节点构成初始候选集合 S。但是深度 d 最多会有 2^{d} 个节点(我们设根节点的深度为 0)不能直接暴力枚举。我们对于每一个节点 u,维护 \text{cnt}_u 表示以 u 为根的子树中包含的候选节点数量。(这个无疑是好维护的,可以用类似求子树大小的方式去求。)\ 那么我们选择一个节点 u 使得 \max(\text{cnt}_u, |S|-\text{cnt}_u) 尽可能小,模拟重心的概念我们也称这个 u 为重心。那么我们不断得到这样的重心,每一次查询 \text{ask}(u,D-d_u)

那么这样查询次数是 O(\log n) 的,是可以的。 ::::success[P17121]

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 3e4 + 5;
int g[N],to[N],nxt[N],ecnt;
int f[N],d[N],n;
int c[N],fx[N],sz;
int tin[N],tout[N],tmr;
int cnt[N],isc[N];

void add(int u,int v) { to[++ecnt] = v; nxt[ecnt] = g[u]; g[u] = ecnt; }

void dfs(int u,int fa) {
    tin[u] = ++tmr;
    d[u] = fa ? d[fa] + 1 : 0;
    for(int e = g[u];e;e = nxt[e]) {
        int v = to[e];
        if(v != fa) dfs(v, u);
    }
    tout[u] = tmr;
}

int ask(int u,int k){
    printf("? %lld %lld\n",u,k);
    fflush(stdout);
    int ans;
    if(scanf("%lld",&ans) != 1) exit(0);
    return ans;
}

signed main(){
    int t;
    scanf("%lld",&t);
    while(t --) {
        scanf("%lld",&n);
        for(int i = 1;i <= n;i ++) g[i] = 0;
        ecnt = 0;
        for(int i = 2;i <= n;i ++) {
            scanf("%lld",&f[i]);
            add(f[i],i);
        }
        tmr = 0;
        dfs(1,0);

        int l = 0,r = n;
        while(l < r) {
            int mid = (l + r) >> 1;
            if(ask(1,mid)) r = mid;
            else l = mid + 1;
        }
        int D = l;
        sz = 0;
        for(int i = 1;i <= n;i ++) if(d[i] == D) c[sz ++] = i;
        while(sz > 1) {
            for(int i = 1;i <= n;i ++) { cnt[i] = 0; isc[i] = 0; }
            for(int i = 0;i < sz;i ++) isc[c[i]] = 1;
            for(int i = n;i >= 1;i --) {
                cnt[i] += isc[i];
                if(f[i]) cnt[f[i]] += cnt[i];
            }
            int u = -1, best = sz + 1;
            for(int i = 1;i <= n;i ++) {
                if(cnt[i] == 0 || cnt[i] == sz) continue;
                int val = max(cnt[i], sz - cnt[i]);
                if(val < best) { best = val; u = i; }
            }
            int o = ask(u,D - d[u]); 
            int nsz = 0;
            for(int i = 0;i < sz;i ++) {
                int x = c[i];
                bool in = (tin[u] <= tin[x] && tin[x] <= tout[u]);
                if(in == (o == 1)) fx[nsz ++] = x;
            }
            for(int i = 0;i < nsz;i ++) c[i] = fx[i];
            sz = nsz;
        }
        printf("! %lld\n", c[0]);
        fflush(stdout);
    }
    return 0;
}

::::

走亲访友

一条边在被删除之前,允许被多次经过。那么对于边 (u,v),如果我们走了 x 遍,相当于我们新建了 x-1 条 (u,v) 的重边。那么我们需要解决的问题就是,要在原图上添加若干重边,使整张图成为欧拉图,然后跑欧拉回路。\ 题目中要求了最后需要保留一棵树。那么聪明的你可能会想到最小生成树,但是题目中没有限制选的边,所以事实上任意一个生成树即可。那么我们

原图所有点的度数不一定都是偶数。欧拉回路要求所有点度数为偶数。我们需要通过添加重边来调整奇度点。\ 那么我们对于生成树自底而上遍历。那么我们一旦遇到了奇度点,我们需要一条重边 u\to \text{fa}_u,这样 u 就是偶度点了。\ 这样是正确的,原来有 m 条边,重边最多 n-1 条,所以欧拉回路总边数不超过 m+n-1。题目保证 k\ge n+m,而 m+n−1<n+m\le k,因此路径长度一定满足限制。 ::::success[P11762]

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 2e6 + 5;
typedef pair<int,int> pii;

int n,m,u,v,k,s,cnt,sum;
vector<pii> g[N];
int d[N],e[N],cur[N];
int t[N],x[N],vis[N],vis2[N];
pii ans[N];

void build(int u) {
    vis[u] = 1;
    int sz = g[u].size();
    for(int i = 0;i < sz;i ++) {
        int v = g[u][i].first;
        int id= g[u][i].second;
        if(x[id] || vis[v]) continue;
        build(v);
        t[e[id]] = 1;
        if(d[v] & 1) {
            cnt ++;
            g[v].push_back({u,m + cnt});
            g[u].push_back({v,m + cnt});
            e[m + cnt] = e[id];
            x[m + cnt] = 1;
            d[v] ++;
            d[u] ++; 
        }
    } 
}

void dfs(int u){
    int len = g[u].size();
    for(int &i = cur[u];i < len;i ++){
        int v = g[u][i].first;
        int id= g[u][i].second;
        if(vis2[id]) continue; 
        vis2[id] = 1;
        dfs(v);
        ans[++sum] = {e[id],t[e[id]]};
    }
}

signed main(){
    scanf("%lld%lld%lld%lld",&n,&m,&k,&s);
    for(int i = 1;i <= m;i ++){
        scanf("%lld%lld",&u,&v);
        g[u].push_back({v,i});
        g[v].push_back({u,i});
        e[i] = i;
        d[u] ++;
        d[v] ++;
    }
    build(s);
    dfs(s);
    printf("%lld\n",sum);
    for(int i = sum;i >= 1;i --) printf("%lld %lld\n",ans[i].first,ans[i].second);
    return 0;
}

::::

Edges in MST

看到 MST 考虑 Kruskal。在 Kruskal 的逻辑里,排序后权值为 w 的边能否进入 MST,只取决于它连接的两个端点在当前并查集中是否属于同一连通块,与后面的边无关。\ 那么我们排序之后把权值 w 的边放在一起处理。如果已经在同一连通块:说明 u\to v 存在一条只由权值 <w 的边构成的路径。那么这条边就可以被更小的边替代,因此不在任何 MST 中,故它是 none。不然的话它一定存在于某一个 MST。 ::::info[证明]{open} 假设存在一棵 MST 包含 (u,v,w)。我们从 MST 中删除 (u,v,w),树分裂成两个连通分量 A,B,且 u\in A,v\in B。\ 因为 u 和 v 通过一条只由权值 <w 的边构成的路径 P 连通,所以 P 从 A 中的 u 出发,到 B 中的 v 结束。路径 P 必然经过边 e=(x,y,w'),其中 x\in A,y\in B,w'<w。考虑

T'=T-\{(u,v,w)\}+e :::: 接下来的问题就是判断剩下的 $(u,v,w)$ 是不是所有的 MST 都必然包含。我们把剩下的所有的 $(u,v,w)$ 建图,图里的割边由于不能被别的边替代(我必须走)所以是 `any`,其他的都是 `at least one`。 ::::success[CF160D] ```c++ #include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; typedef pair<int,int> pii; struct DSU { int fa[N]; void init(int n) { for(int i = 1;i <= n;i ++) fa[i] = i; } int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } void merge(int x,int y) { int fx = find(x); int fy = find(y); if(fx != fy) fa[fx] = fy; } bool same(int x,int y) { return find(x) == find(y); } }dsu; struct Edge { int u,v,w,id; }B[N]; bool cmp(Edge a,Edge b) { return a.w < b.w; } int n,m; int dfn[N],low[N],tim; vector<pii> g[N]; int ans[N]; void tarjan(int u,int fa) { dfn[u] = low[u] = ++ tim; for(auto now : g[u]) { int v = now.first; int id = now.second; if(id == fa) continue; if(!dfn[v]) { tarjan(v,id); low[u] = min(low[u],low[v]); if(low[v] > dfn[u]) ans[id] = 2; } else { low[u] = min(low[u],dfn[v]); } } } signed main() { scanf("%lld%lld",&n,&m); for(int i = 1;i <= m;i ++) { int u,v,w; scanf("%lld%lld%lld",&u,&v,&w); B[i] = {u,v,w,i}; } sort(B + 1,B + m + 1,cmp); dsu.init(n); for(int i = 1;i <= m;) { int j = i; while(j <= m && B[j].w == B[i].w) j ++; for(int k = i;k < j;k ++) { int u = dsu.find(B[k].u); int v = dsu.find(B[k].v); int id = B[k].id; if(u == v) ans[id] = 0; else ans[id] = 1; } for(int k = i;k < j;k ++) { int u = dsu.find(B[k].u); int v = dsu.find(B[k].v); if(u == v) continue; int id = B[k].id; g[u].push_back({v,id}); g[v].push_back({u,id}); } for(int k = i;k < j;k ++) { int u = dsu.find(B[k].u); if(!dfn[u]) tarjan(u,0); } for(int k = i;k < j;k ++) { int u = dsu.find(B[k].u); int v = dsu.find(B[k].v); if(u != v) dsu.merge(u,v); } for(int k = i;k < j;k ++) { int u = dsu.find(B[k].u); int v = dsu.find(B[k].v); dfn[u] = dfn[v] = 0; g[u].clear(); g[v].clear(); } i = j; } for(int i = 1;i <= m;i ++) { if(ans[i] == 0) puts("none"); else if(ans[i] == 1) puts("at least one"); else puts("any"); } return 0; } ``` :::: ## [Counting Shortcuts](https://codeforces.com/problemset/problem/1650/G) 我们设 $\text{dist}_{s,u}$ 表示 $s\to u$ 的最短距离,$\text{dist}_{t,u}$ 表示 $u\to t$ 的最短距离。由于我们要找的路径长度和最短路不超过 $1$,所以最后一定是这样的形式: $$s\xrightarrow{最短路}u\to v\xrightarrow{最短路}t$$ 其中 $u$ 和 $v$ 满足 $\text{dist}_{s,u}=\text{dist}_{s,v}$,且 $\text{dist}_{s,u}+\text{dist}_{t,v}=d$ 这样总长度就是 $\text{dist}_{s,u}+\text{dist}_{t,v}+w(u,v)=d+1$。\ 由于每一条边的边权都为 $1$,每条长度为 $d+1$ 的路径都对应一条同层边的必要性,充分性显然。 ::::success[CF1650G] ``` #include<bits/stdc++.h> #define int long long using namespace std; const int N = 2e5 + 5; typedef pair<int,int> pii; const int Mod = 1e9 + 7; int n,m,s,t,T,ans; vector<int> g[N]; pii B[N]; int dist[3][N],f[3][N],u,v; priority_queue<pii,vector<pii>,greater<pii> > q; void dijkstra(int s,int op) { for(int i = 1; i <= n; i++) { dist[op][i] = -1; f[op][i] = 0; } dist[op][s] = 0; f[op][s] = 1; q.push({0,s}); while(!q.empty()) { int d = q.top().first; int u = q.top().second; q.pop(); if(d > dist[op][u]) continue; for(auto v : g[u]) { if(dist[op][v] == -1 || dist[op][v] > dist[op][u] + 1) { dist[op][v] = dist[op][u] + 1; f[op][v] = f[op][u]; q.push({dist[op][v],v}); } else if(dist[op][v] == dist[op][u] + 1) { f[op][v] = (f[op][v] + f[op][u]) % Mod; } } } } signed main() { scanf("%lld",&T); while(T--) { scanf("%lld%lld%lld%lld",&n,&m,&s,&t); for(int i = 1;i <= n;i++) g[i].clear(); for(int i = 1;i <= m;i++) { scanf("%lld%lld",&u,&v); B[i] = {u,v}; g[u].push_back(v); g[v].push_back(u); } dijkstra(s,1); dijkstra(t,2); ans = f[1][t]; for(int i = 1;i <= m;i++) { int u = B[i].first; int v = B[i].second; if(dist[1][u] == dist[1][v]) { if(dist[1][u] + dist[2][v] == dist[1][t]) ans = (ans + f[1][u] * f[2][v]) % Mod; if(dist[1][v] + dist[2][u] == dist[1][t]) ans = (ans + f[1][v] * f[2][u]) % Mod; } } printf("%lld\n",ans); } return 0; } ``` :::: ## [Hamu](https://www.luogu.com.cn/problem/P17122) 由于最后要求最终遍历任意一个城市偶数遍,所以我们设到达城市 $i$ 的次数为 $v_i$,那么 $v_i$ 与 $a_i$ 同奇偶。我们设总到达次数为 $k$(即行走步数),则 $$\sum_iv_i=k\Rightarrow⨁_iv_i=k\bmod 2$$ 设 $b_i=a_i\bmod 2$,那么设 $X=⨁_i b_i$,可以得到 $k\bmod 2=X$。那么 $k \bmod 2 = X$。首先我们解决显然无解的情况。 - 存在从 $s$ 出发到达不了的城市 $u$ 满足 $a_u$ 为奇数。 - 若 $s$ 所在连通分量是二分图。其中所有环长度为偶数,因此任何从 $s$ 出发回到 $s$ 的闭合行走长度必为偶数。所以若该分量为二分图且 $X=1$,则需要 $k$ 为奇数,此时无解。 我们先构造出来一个生成树,然后沿着每条树边被向下、向上各走一次。即从 $s$ 出发,首先我们先走 $v\to \dots \to u$,其中 $v$ 是非根节点,中间省略的是 $v$ 在生成树里面的子树,最终遍历整棵树后回到 $s$。那么 - 对于每一个非根节点 $v$ 会被遍历 $1+\text{son}_v$ 次。 - 对于根结点由于没有父亲会被遍历 $\text{son}_v$ 次。 设 $c_u$ 是当前构造的行走中,城市 $u$ 已经被访问次数的奇偶性。我们按后序遍历每个非根节点 $v$,当回溯到 $v$ 时,检查 $c_v,b_v$ 是否相等,如果不相等,那么我们走一次 $u\to v\to u$,翻转 $c_u,c_v$ 的状态即可。\ 那么我们基础遍历每条树边走两次,共 $2(n-1)$ 步。每个非根节点最多修正一次,每次两步,最多 $2(n-1)$ 步。总长度不超过 $4n-4<5n$。 ::::info[根节点自动匹配] 除了根节点 $s$ 都有 $c_u=b_u$。\ 行走总到达次数等于步数 $k$,所以 $⨁c_i=k\bmod 2=X$,$⨁_{i≠s}b_i=X⨁b_s$,那么 $$ \left(X⨁b_s\right)⨁c=X\\ \Downarrow\\ \left(X⨁b_s\right)⨁c_s=X\Rightarrow b_s=c_s $$ :::: 当 $X=1$ 时,生成树遍历总长度为偶数,需要额外走一次奇环。\ 由于图不是二分图,我们进行一个 DFS 遍历所有的边,遍历的时候一定会遇到一个非树边 $(u,v)$ 满足 $v$ 是 $u$ 的祖先,深度奇偶性相同。那么我们树上的路径 $u\to v$ 的长度是偶数,加上 $(u,v)$ 就是一个奇环。那么我们翻转奇环路径上的 $b_i$,在答案序列中找到 $v$ 第一次出现的位置,记为下标 $k$,在其后插入奇环路径。这样问题就解决了。 ::::success[P17122] ```c++ #include<bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n,m,s,T,X,eu,ev; vector<int> G[N]; int a[N],b[N],c[N],vis[N],col[N],fa[N],dep[N]; vector<int> ans,tmp; queue<int> q; void BFS(){ q.push(s); vis[s] = 1; while(!q.empty()){ int u = q.front(); q.pop(); for(auto v : G[u]){ if(!vis[v]){ vis[v] = 1; q.push(v); } } } } int Dfs(int u,int f){ vis[u] = 1; fa[u] = f; dep[u] = dep[f] + 1; for(auto v : G[u]){ if(v == f) continue; if(!vis[v]){ if(Dfs(v,u)) return 1; }else if(v == u || (dep[v] < dep[u] && ((dep[v] & 1) == (dep[u] & 1)))){ eu = u; ev = v; return 1; } } return 0; } void build(int u,int f){ for(auto v : G[u]){ if(v == f || vis[v]) continue; vis[v] = 1; ans.push_back(v); c[v] ^= 1; build(v,u); ans.push_back(u); c[u] ^= 1; if(c[v] != b[v]){ ans.push_back(v); c[v] ^= 1; ans.push_back(u); c[u] ^= 1; } } } void solve(){ scanf("%d%d%d",&n,&m,&s); while(!q.empty()) q.pop(); for(int i = 1;i <= n;i++){ scanf("%d",&a[i]); G[i].clear(); vis[i] = 0; c[i] = 0; fa[i] = 0; dep[i] = 0; } for(int i = 1;i <= m;i++){ int u,v; scanf("%d%d",&u,&v); G[u].push_back(v); G[v].push_back(u); } BFS(); int bt = 1; for(int i = 1;i <= n;i++){ if(!vis[i] && (a[i] & 1)) bt = 0; } if(!bt){ puts("No"); return; } X = 0; for(int i = 1;i <= n;i++){ b[i] = vis[i] ? (a[i] & 1) : 0; X ^= b[i]; } ans.clear(); if(X){ for(int i = 1;i <= n;i++) vis[i] = 0; dep[0] = 0; if(!Dfs(s,0)){ puts("No"); return; } for(int x = eu;;x = fa[x]){ b[x] ^= 1; if(x == ev) break; } } for(int i = 1;i <= n;i++) vis[i] = 0; vis[s] = 1; build(s,0); if(X){ tmp.clear(); for(int x = eu;x != ev;x = fa[x]) tmp.push_back(x); reverse(tmp.begin(),tmp.end()); tmp.push_back(ev); int k = -1; for(int i = 0;i < (int)ans.size();i++){ if(ans[i] == ev){ k = i; break; } } ans.insert(ans.begin() + k + 1,tmp.begin(),tmp.end()); for(auto v : tmp) c[v] ^= 1; } for(int i = 1;i <= n;i++){ if(c[i] != (a[i] & 1)){ puts("No"); return; } } if((int)ans.size() > 5 * n){ puts("No"); return; } printf("Yes\n%d\n",(int)ans.size()); for(auto v : ans) printf("%d ",v); puts(""); } signed main(){ scanf("%d",&T); while(T--) solve(); return 0; } ``` ::::