先用并查集缩掉所有 t=2 的相等点,再只保留 t=0 的不同色森林;计数是森林染色,最小和是带点权二分染色。
OJ: luogu
题目 ID: P7846
难度:提高+/省选-
标签:并查集树图论计数二分图染色
日期: 2026-06-21 03:40
题意
给一棵树,每条边有三种关系:
:两端点权必须不同 :没有要求 :两端点权必须相同
每个点权
要求输出:
- 合法序列数量(对
取模) - 所有合法序列里
的最小值;若无解输出
思路
先看一个可以直接验证想法的朴素解:
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 直接枚举所有点权,再检查每条边的约束。
这个做法完全正确,但复杂度是
真正的关键是先把三种边分开看。
缩点后:
- 每个新点对应一个“必须取同一个值”的块
- 这个块的权重就是它包含多少个原点
再看剩余边:
没有限制,可以直接忽略 只要求两个块取值不同
这样问题就被化成了一片森林上的约束。
缩点后的理解
这张图展示的是:先把
graph G {
A [label="一个缩点块"];
B [label="另一个缩点块"];
A -- B [label="t=0"];
}
缩点块内部所有原点必须同值,所以内部不再需要单独讨论。
剩下的
第一问就变成森林 proper coloring 计数。
对于一个
- 第一个点有
种选法 - 之后每条边往下扩展时都有
种
所以若这个连通块有
所有连通块相乘即可。
第二问则更简单。
因为森林一定是二分图,所以想让总和最小,只需要使用最小的两种值
- 一侧放
- 另一侧放
最优代价就是:
也就是让较大的那一侧用
对于没有任何
代码
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;
}复杂度
并查集、建图、染色都只需要线性级别处理。
总时间复杂度是
总结
这题最核心的拆分是:
先缩点 直接忽略 变成森林上的不同色约束
这样第一问变成森林染色计数,第二问变成带点权二分染色最小代价。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。


