「杂题记录」彩色挂饰
一道 dp + 图论 + 状压好题
「杂题记录」彩色挂饰
题意简述
给定一张 \(n\) 个点 \(m\) 条边的无向图,有一些点有颜色,定义颜色相同的点组成的连通块为同色连通块。需要对没有颜色的点进行染色,最小化同色连通块的数量。
无向图满足每个点双的大小 \(\le s\) ,给出的颜色分别从为 \(1\) 到 \(k\) 编号。
\(n\le 10^5, 2<k\le 20,s\le 6, n-1\le m < ns\)
对于 \(10\) % 的数据,\(s = 2\).
分析
\(s=2\) 是一档很有启发性的部分分,可以感受到这就是一个树。
不妨设根节点编号为 \(1\) 直接树形 dp,设 \(f(u, c)\) 为把以 \(u\) 为根节点的子树,\(u\) 染成颜色 \(c\) ,其余的任意染,最小的同色连通块数量。 \[ f(u, c) =1+\sum_{(u, v) \in E}\left(f(v, c) - 1\right) \] 一个图怎么做?考虑用圆方树将无向图转化为树,然后进行树形 dp。 \(f(u, c)\) 的定义转化为以 \(u\) 为根的子树,圆点 或 方点的父亲染成的颜色。感觉是一类圆方树上 dp 的标准套路。 对于圆点: \[ f(u, c) =1+\sum_{(u, v) \in E}\left(f(v, c) - 1\right) \] 对于方点: 简单dp 一下就可以了。 注意到 \(s\le 6\) ,瞎搞一下就可以了。注意以下的集合均指每个将一个点双上的点编号离散后的编号点集。
设:定义在点上的函数 \(A(x)\) 为与 \(x\) 相连的点集。
设:定义在集合上的函数 \(C(S)\) ,表示点集 \(S\) 是否连通。 \[ \begin{split} C(\varnothing) &= 1\\ C(S) &= \or_{x\in S}(C(S / \{x\}) \operatorname{AND}(A(x) \cap S \not = \varnothing)) \end{split} \]
设:函数 \(G(S, c)\) 为将连通块 \(S\) 涂成 \(c\) ,最小代价。每个连通块对应的子树也算入答案。 \[ G(S, c)= \begin{cases} \operatorname{INF}, & C(S)=0 \\ 1+\sum_{x\in S}\limits{(f(x, c)-1)} \end{cases} \] 设:函数\(H(S)\) 为将连通块 \(S\) 涂上任意一种相同的颜色,最小代价。 \[ H(S) = \min_{c=1}^{k}\limits{G(S, c)}. \] 设:函数 \(L(S)\) 为将连通块 \(S\) 切成若干块,每一块涂上相同的颜色,最小代价。 \[ L(S) = \min\{H(S),\ \min_{s \subseteq S} L(s)+L(S / s)\} \] 就可以求出 \[ f(u, c)= \min_{S, u \in S}\limits{G(S, c) + L(S_{补})} \] 转移只需要 80 行就写完了。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
#include <cassert>
#include <set>
using namespace std;
const int _ = 4e5 + 100;
const int _K = 30;
int Col[_];
int n, m, k, s;
int head[_];
struct edges{ int node, nxt; } edge[_ << 1]; int tot = 0;
void add(int u, int v){ tot++; edge[tot].node = v; edge[tot].nxt = head[u]; head[u] = tot; }
vector<int> G[_];
set<int> GG[_];
int dfn[_], low[_], dfc = 0, tmp;
int cnt;
stack<int> S;
void tarjan(int now) {
dfn[now] = low[now] = ++dfc; S.push(now);
for(int i = 0; i < (int)G[now].size(); i++) {
int ex = G[now][i];
if(!dfn[ex]) {
tarjan(ex);
low[now] = min(low[now], low[ex]);
if(dfn[now] == low[ex]) {
++cnt;
add(now, cnt); add(cnt, now);
do add(cnt, tmp = S.top()), add(tmp, cnt), S.pop(); while(tmp != ex);
}
} else low[now] = min(low[now], dfn[ex]);
}
}
int popcnt(int S) { int ans = 0; while(S) ans += ((S & 1) != 0), S >>= 1; return S; }
int F[_][_K];
const int _S = 15;
int ver = 0;
int _INT_MAX_;
void d(int now, int fa){
for(int i = head[now]; i; i = edge[i].nxt) {
int ex = edge[i].node; if(ex == fa) continue;
d(ex, now);
}
if(now <= n) {
for(int i = 1; i <= k; i++) {
if(Col[now]) if(Col[now] != i) { F[now][i] = _INT_MAX_; continue; }
int &ans = F[now][i] = 1;
for(int j = head[now]; j; j = edge[j].nxt) {
if(edge[j].node == fa) continue;
ans += F[edge[j].node][i] - 1;
}
}
} else {
static int Link[_S], iLink[_], NodeCnt; NodeCnt = 0;
for(int i = head[now]; i; i = edge[i].nxt) {
int ex = edge[i].node;
Link[++NodeCnt] = ex;
iLink[ex] = NodeCnt;
}
static int A[_S]; static bool C[1 << _S];
for(int i = 1; i <= NodeCnt; i++) {
int &ans = A[i] = 0;
for(int j = 1; j <= NodeCnt; j++){ if(i == j) continue;
int ex = Link[j]; if(GG[Link[i]].find(ex) == GG[Link[i]].end()) continue;
ans |= (1 << (j - 1));
}
}
for(int i = 0; i < (1 << NodeCnt); i++) C[i] = 0;
C[0] = 1; // C[S] 重标号后的点集 S 是否连通.
for(int i = 1; i <= NodeCnt; i++) C[1 << (i - 1)] = 1;
for(int i = 1; i < (1 << NodeCnt); i++){
bool &ans = C[i];
for(int j = 1; j <= NodeCnt; j++){
if(i & (1 << (j - 1))) ; else continue;
ans = ( ans || (C[i ^ (1 << (j - 1))] && ((A[j] & i) != 0)) );
}
}
static int G[1 << _S][_K]; // G[S][c] 把联通块 S 涂上颜色 c. 需要的次数.
for(int i = 1; i < (1 << NodeCnt); i++){
for(int j = 1; j <= k; j++){
int &ans = G[i][j];
if(!C[i]) { ans = _INT_MAX_; continue; }
ans = 1;
for(int l = 1; l <= NodeCnt; l++){
if(i & (1 << (l - 1))) if(l != iLink[fa]) ans += F[Link[l]][j] - 1;// , assert(F[Link[l]][j] > 0);
}
}
}
static int H[1 << _S]; // H[S] 把 连通块 S 涂上同一种颜色 所需要的 最小代价。
H[0] = 0;
for(int i = 1; i < (1 << NodeCnt); i++){
int &ans = H[i] = _INT_MAX_;
for(int j = 1; j <= k; j++) ans = min(ans, G[i][j]);
}
static int L[1 << _S]; L[0] = 0; // L[S] 把 连通块 S 分成若干块,每一块分别涂上相同的颜色 最小代价。
for(int i = 1; i < (1 << NodeCnt); i++){
int &ans = L[i] = H[i];
for(int S0 = i; S0; S0 = (S0 - 1) & i){
ans = min(ans, L[S0] + L[i ^ S0]);
}
}
for(int i = 1; i <= k; i++){
int &ans = F[now][i] = _INT_MAX_;
if(Col[fa]) if(Col[fa] != i) continue;
int S = (1 << (NodeCnt)) - 1; S ^= (1 << (iLink[fa] - 1));
ans = min(ans, G[(1 << (iLink[fa] - 1))][i] + L[S]);
for(int j = S; j ; j = (j - 1) & S){
ans = min(ans, G[j | (1 << (iLink[fa] - 1))][i] + L[S ^ j]);
}
}
}
}
int main(){
// freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false);
cin >> n >> m >> k >> s; cnt = n; _INT_MAX_ = 3 * n;
for(int i = 1; i <= n; i++) cin >> Col[i];
for(int i = 1; i <= m; i++){
int u, v; cin >> u >> v;
G[u].push_back(v); GG[u].insert(v);
G[v].push_back(u); GG[v].insert(u);
}
tarjan(1);
d(1, 1);
int ans = _INT_MAX_;
for(int i = 1; i <= k; i++) ans = min(ans, F[1][i]);
printf("%d", ans);
return 0;
}