「dWoi R2」Arcade hall / 街机厅

GitHub跳转原题关系图返回列表

先用并查集缩掉所有 t=2 的相等点,再只保留 t=0 的不同色森林;计数是森林染色,最小和是带点权二分染色。

OJ: luogu

题目 ID: P7846

难度:提高+/省选-

标签:并查集图论计数二分图染色

日期: 2026-06-21 03:40

题意

给一棵树,每条边有三种关系:

  • 00:两端点权必须不同
  • 11:没有要求
  • 22:两端点权必须相同

每个点权 wiw_i 都在 [1,R][1,R] 内。

要求输出:

  • 合法序列数量(对 109+710^9+7 取模)
  • 所有合法序列里 wi\sum w_i 的最小值;若无解输出 00

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 12;
const long long MOD = 1000000007LL;

int n, r_limit;
int eu[MAXN], ev[MAXN], et[MAXN];
int w[MAXN];
long long ways;
long long best_sum;

void dfs(int u) {
    if (u > n) {
        for (int i = 1; i < n; i++) {
            if (et[i] == 0 && w[eu[i]] == w[ev[i]]) {
                return;
            }
            if (et[i] == 2 && w[eu[i]] != w[ev[i]]) {
                return;
            }
        }

        ways = (ways + 1) % MOD;
        long long sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += w[i];
        }
        best_sum = min(best_sum, sum);
        return;
    }

    for (int val = 1; val <= r_limit; val++) {
        w[u] = val;
        dfs(u + 1);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // brute.cpp:小数据暴力枚举所有点权,检查约束并统计答案。
    cin >> n >> r_limit;
    for (int i = 1; i < n; i++) {
        cin >> eu[i] >> ev[i] >> et[i];
    }

    ways = 0;
    best_sum = (1LL << 60);
    dfs(1);

    if (ways == 0) {
        cout << 0 << ' ' << 0 << '\n';
    } else {
        cout << ways % MOD << ' ' << best_sum << '\n';
    }
    return 0;
}

brute.cpp 直接枚举所有点权,再检查每条边的约束。 这个做法完全正确,但复杂度是 RnR^n,只能处理很小的数据。

真正的关键是先把三种边分开看。

t=2t=2 表示两端必须相等,所以可以先把这些点全部缩成一个并查集块。

缩点后:

  • 每个新点对应一个“必须取同一个值”的块
  • 这个块的权重就是它包含多少个原点

再看剩余边:

  • t=1t=1 没有限制,可以直接忽略
  • t=0t=0 只要求两个块取值不同

这样问题就被化成了一片森林上的约束。

缩点后的理解

这张图展示的是:先把 t=2t=2 边缩掉,剩下只有“必须不同”的边真正有约束。

graph G {
  A [label="一个缩点块"];
  B [label="另一个缩点块"];
  A -- B [label="t=0"];
}

缩点块内部所有原点必须同值,所以内部不再需要单独讨论。 剩下的 t=0t=0 边只是在这些块之间做“不同色”限制。

第一问就变成森林 proper coloring 计数。

对于一个 t=0t=0 连通块:

  • 第一个点有 RR 种选法
  • 之后每条边往下扩展时都有 R1R-1

所以若这个连通块有 ss 个缩点,方案数就是:

R(R1)s1R \cdot (R-1)^{s-1}

所有连通块相乘即可。

第二问则更简单。

因为森林一定是二分图,所以想让总和最小,只需要使用最小的两种值 1122。 设某个连通块二分染色后两侧原点数总和分别是 A,BA,B,那么:

  • 一侧放 11
  • 另一侧放 22

最优代价就是:

A+B+min(A,B)A + B + \min(A, B)

也就是让较大的那一侧用 11,较小的一侧用 22

对于没有任何 t=0t=0 约束的孤立块,直接全部取 11 即可。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100000 + 5;
const long long MOD = 1000000007LL;

struct Edge {
    int u, v, t;
};

int n, r_limit;
Edge edges[MAXN];

int dsu_parent[MAXN];
int dsu_size[MAXN];
int comp_id[MAXN];
int comp_weight[MAXN];
int comp_cnt;

vector<int> g[MAXN];
int color_part[MAXN];

int find_root(int x) {
    if (dsu_parent[x] == x) {
        return x;
    }
    dsu_parent[x] = find_root(dsu_parent[x]);
    return dsu_parent[x];
}

void unite(int a, int b) {
    a = find_root(a);
    b = find_root(b);
    if (a == b) {
        return;
    }
    if (dsu_size[a] < dsu_size[b]) {
        swap(a, b);
    }
    dsu_parent[b] = a;
    dsu_size[a] += dsu_size[b];
}

long long mod_pow(long long a, int e) {
    long long res = 1;
    while (e > 0) {
        if (e & 1) {
            res = res * a % MOD;
        }
        a = a * a % MOD;
        e >>= 1;
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> r_limit;
    for (int i = 1; i <= n; i++) {
        dsu_parent[i] = i;
        dsu_size[i] = 1;
    }

    for (int i = 1; i < n; i++) {
        cin >> edges[i].u >> edges[i].v >> edges[i].t;
        if (edges[i].t == 2) {
            unite(edges[i].u, edges[i].v);
        }
    }

    for (int i = 1; i <= n; i++) {
        comp_id[i] = 0;
        comp_weight[i] = 0;
        g[i].clear();
        color_part[i] = -1;
    }

    for (int i = 1; i <= n; i++) {
        int root = find_root(i);
        if (comp_id[root] == 0) {
            comp_id[root] = ++comp_cnt;
        }
        int id = comp_id[root];
        comp_weight[id]++;
    }

    int zero_edge_cnt = 0;
    for (int i = 1; i < n; i++) {
        if (edges[i].t != 0) {
            continue;
        }
        int a = comp_id[find_root(edges[i].u)];
        int b = comp_id[find_root(edges[i].v)];
        // 在树上缩掉 t=2 后,不会出现 a == b 的情况;这里保守防一下。
        if (a == b) {
            cout << 0 << ' ' << 0 << '\n';
            return 0;
        }
        g[a].push_back(b);
        g[b].push_back(a);
        zero_edge_cnt++;
    }

    if (r_limit == 1 && zero_edge_cnt > 0) {
        cout << 0 << ' ' << 0 << '\n';
        return 0;
    }

    long long ways = 1;
    long long min_sum = 0;
    for (int i = 1; i <= comp_cnt; i++) {
        if (color_part[i] != -1) {
            continue;
        }

        long long part_sum[2] = {0, 0};
        int edge_in_component = 0;
        queue<int> q;
        q.push(i);
        color_part[i] = 0;

        while (!q.empty()) {
            int u = q.front();
            q.pop();
            part_sum[color_part[u]] += comp_weight[u];
            edge_in_component += (int)g[u].size();

            for (size_t j = 0; j < g[u].size(); j++) {
                int v = g[u][j];
                if (color_part[v] == -1) {
                    color_part[v] = color_part[u] ^ 1;
                    q.push(v);
                }
            }
        }

        if (edge_in_component == 0) {
            // 没有敌对边约束的独立点,直接放最小值 1 即可。
            ways = ways * r_limit % MOD;
            min_sum += part_sum[0];
        } else {
            int edge_cnt = edge_in_component / 2;
            ways = ways * r_limit % MOD;
            ways = ways * mod_pow(r_limit - 1, edge_cnt) % MOD;
            // 这个连通块一定是树,最优只需要用颜色 1 和 2,
            // 再把较小的一侧放颜色 2。
            min_sum += part_sum[0] + part_sum[1] + min(part_sum[0], part_sum[1]);
        }
    }

    cout << ways % MOD << ' ' << min_sum << '\n';
    return 0;
}

复杂度

并查集、建图、染色都只需要线性级别处理。

总时间复杂度是 O(nα(n))O(n \alpha(n)),空间复杂度是 O(n)O(n)

总结

这题最核心的拆分是:

  • t=2t=2 先缩点
  • t=1t=1 直接忽略
  • t=0t=0 变成森林上的不同色约束

这样第一问变成森林染色计数,第二问变成带点权二分染色最小代价。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析