覆盖 / 支配 / 染色
三状态·染色计数
本课摘要
覆盖 / 支配 / 染色课程回答“覆盖、支配和染色约束需要哪些互斥状态”。内容以三状态·染色计数为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断覆盖 / 支配 / 染色的适用条件与状态边界
- 围绕“三状态·染色计数”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
当「被覆盖」比「选没选」更微妙
上一批的 里,一个点只有「选 / 不选」两态。但支配集把要求提高了一档: 在树上放最少(或最省钱)的警卫,让每个点要么自己是警卫、要么与某个警卫相邻——全树被「支配」。
难点在这:一个没放警卫的点,它到底被覆盖了没有?可能被某个孩子覆盖(孩子放了警卫), 也可能孩子都没放、只能指望父亲来覆盖它。这两种「没放警卫」的处境后果完全不同,两态不够用了。
三状态:把「谁来覆盖 u」记进状态
为每个点 开三态,造价意义下取最小:
读作: = u 放警卫; = u 不放、但被某个孩子覆盖; = u 不放、也没被孩子覆盖(把覆盖它的责任留给父亲)。转移分三路:
u 放了警卫,它顺手覆盖所有孩子,于是每个孩子三态随便取最小(包括孩子的「等父亲」态——因为 u 就是那个父亲)。
u 不放、也不靠孩子,那每个孩子必须自给自足(自己放警卫,或被它自己的孩子覆盖),不能是「等父亲」态 ——因为 u 自身都没被覆盖,救不了孩子。
的基线和 一样(孩子自足),但额外强制至少一个孩子放警卫来覆盖 u——取「把某个孩子从自足抬到放警卫」的最小增量加上去。
本质
「点覆盖」盯的是边(每条边有人守),「支配集」盯的是点(每个点被支配)。后者的困难全在——没放警卫的点,覆盖它的责任可能来自孩子、也可能来自父亲。把这个「责任方向」显式记成第三态,DFS 才能在后序时正确结算。三态是支配集类题的通用骨架。
跟着算一遍
小树:根 带 ; 带一个孩子 。造价 。后序 :
看三状态逐点点亮
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
另一面:染色计数,同时求 max 与 min
覆盖类还有一支是染色计数。三色二叉树:每个节点涂红 / 绿 / 蓝之一,要求父子不同色、兄弟不同色, 问绿色节点数的最大值与最小值各是多少。这不是求方案数,而是在「合法染色」的约束下优化一个计数。
状态自然是 = u 涂 色时、其子树里的绿点数(分别维护 max 与 min)。转移枚举左右孩子的颜色 ,要求 :
同理把 换 。因为只有三种颜色、两个孩子,内层枚举是常数级,整体仍是 。同一份 DFS 同时算出 max 与 min 两个答案。
常见陷阱:极值与方案数别混为一谈
三色二叉树求的是「绿点数的 max/min」这一极值,转移用 ;若题目改问「合法染色的方案数」,则要把内层的取极值换成累乘 + 累加(每种合法 的方案数相乘再对颜色求和)。同一棵树、同一套约束,「求极值」与「数方案」的算子完全不同——下一类 方案数 / 距离统计 专讲后者。
例题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1505;
vector<int> g[N];
int c[N]; // c[u]:在 u 放警卫的造价
long long f[N][3]; // 0:放警卫 1:被某孩子覆盖 2:空着,等父亲来覆盖
bool hasFa[N];
const long long INF = 1e15;
void dfs(int u)
{
f[u][0] = c[u]; // 放警卫:先付自己的造价
f[u][1] = 0; // 被孩子覆盖:下面累加,另需至少一个孩子放警卫
f[u][2] = 0; // 等父亲覆盖:孩子必须自给自足
long long extra = INF; // 把某个孩子从"自足"抬到"放警卫"的最小增量
bool hasChild = false;
for (int v : g[u])
{
hasChild = true;
dfs(v);
f[u][0] += min({f[v][0], f[v][1], f[v][2]}); // u 已覆盖孩子,孩子随意取最小
long long self = min(f[v][0], f[v][1]); // 孩子"自足"(不靠 u)
f[u][1] += self;
f[u][2] += self;
extra = min(extra, f[v][0] - self); // 让这个孩子改放警卫的代价
}
if (!hasChild) f[u][1] = INF; // 叶子无孩子,不可能"被孩子覆盖"
else f[u][1] += extra; // ★强制至少一个孩子放警卫
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
int u, cost, k;
cin >> u >> cost >> k;
c[u] = cost;
while (k--)
{
int v;
cin >> v;
g[u].push_back(v);
hasFa[v] = true;
}
}
int root = 1;
while (root <= n && hasFa[root]) root++;
dfs(root);
cout << min(f[root][0], f[root][1]) << endl; // 根不能停在状态 2
return 0;
}#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
const int N = 500005;
int lc[N], ls[N]; // 用括号串重建二叉树的左右孩子
string s;
int idx;
long long fmax[N][3], fmin[N][3]; // 颜色 0/1/2,其中「绿」(设为 2)计入统计
int build() // 按括号串递归建树,返回当前节点编号
{
int u = ++idx;
char ch = s[u - 1];
lc[u] = ls[u] = 0;
if (ch >= '2') lc[u] = build(); // 有左孩子
if (ch == '2') ls[u] = build(); // 有右孩子('2' 表示两个孩子)
return u;
}
void dfs(int u)
{
if (!u) return;
dfs(lc[u]);
dfs(ls[u]);
for (int col = 0; col < 3; col++)
{
long long addG = (col == 2) ? 1 : 0; // 绿色 +1
// 左右孩子颜色都要与 u 不同;两孩子之间也不同
long long bestMax = -1, bestMin = 1e18;
for (int a = 0; a < 3; a++)
for (int b = 0; b < 3; b++)
{
if (a == col || b == col || a == b) continue;
long long lM = lc[u] ? fmax[lc[u]][a] : 0;
long long rM = ls[u] ? fmax[ls[u]][b] : 0;
long long lm = lc[u] ? fmin[lc[u]][a] : 0;
long long rm = ls[u] ? fmin[ls[u]][b] : 0;
bestMax = max(bestMax, lM + rM);
bestMin = min(bestMin, lm + rm);
}
// 单孩子/叶子时另作简化处理(此处示意主干)
fmax[u][col] = addG + bestMax;
fmin[u][col] = addG + bestMin;
}
}
int main()
{
cin >> s;
idx = 0;
int root = build();
dfs(root);
long long mx = max({fmax[root][0], fmax[root][1], fmax[root][2]});
long long mn = min({fmin[root][0], fmin[root][1], fmin[root][2]});
cout << mx << endl << mn << endl;
return 0;
}
