动态规划
状压DP
- 集合枚举 \(O(3^n)\)
\(S\) 是一个二进制数,表示一个集合,\(S0\) 表示 \(S\) 的子集。
code
//包括S本身
for (int S = 1; S < (1 << n); S ++ )
for (int S0 = S; S0; S0 = (S0 - 1) & S)
//不包括S本身
for (int S = 1; S < (1 << n); S ++ )
for (int S0 = (S - 1) & S; S0; S0 = (S0 - 1) & S)
斯坦纳树
- 问题:给定连通图 \(G\) 中的 \(n\) 个点与 \(k\) 个关键点,连接 \(k\) 个关键点,使得生成树的所有边的权值和最小。
- 状态表示 :设 \(dp(i,S)\) 表示以 \(i\) 为根的一棵树,包含集合 \(S\) 中所有点的最小代价。
- 状态转移:
一棵以 \(i\) 为根的树有两种情况,第一种是 \(i\) 的度数为 \(1\),另一种是 \(i\) 的度数大于 \(1\)。
对于 \(i\) 的度数为 \(1\) 的情况,可以考虑枚举树上与 \(i\) 相邻的点 \(j\),则$$min(dp(i,S),dp(j,S)+w(j,i))→dp(i,S)$$ 对于 \(i\) 的度数大于 \(1\) 的情况,可以划分成几个子树考虑,即:
\[min(dp(i,S),dp(i,T)+dp(i,S−T))→dp(i,S) (T⊆S)
\]
【模板】最小斯坦纳树
\(O(n×3^k+m\, log\, m×2^k)\)
code
#define PII pair<int, int>
#define x first
#define y second
#define mk make_pair
const int N = 510, M = 1010, INF = 0x3f3f3f3f;
int n, m, k;
int h[M], w[M], e[M], ne[M], idx;
int tr[M];
int f[N][5000], st[N];
int p[N];
priority_queue<PII, vector<PII>, greater<PII> > q;
void add(int a, int b, int c)
{
tr[idx] = e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dij(int s)
{
memset(st, 0, sizeof st);
while (!q.empty())
{
PII t = q.top();
q.pop();
if (st[t.y]) continue;
st[t.y] = 1;
for (int i = h[t.y]; ~i; i = ne[i])
{
if (f[tr[i]][s] > f[t.y][s] + w[i])
{
f[tr[i]][s] = f[t.y][s] + w[i];
q.push(mk(f[tr[i]][s], tr[i]));
}
}
}
}
int main()
{
memset(h, -1, sizeof h);
memset(f, 0x3f, sizeof f);
rd(n), rd(m), rd(k);
for (int i = 1, a, b, c; i <= m; i ++ )
{
rd(a), rd(b), rd(c);
add(a, b, c);
add(b, a, c);
}
for (int i = 1; i <= k; i ++ )
{
rd(p[i]);
f[p[i]][1 << (i - 1)] = 0;
}
for (int s = 1; s < (1 << k); s ++ )
{
for (int i = 1; i <= n; i ++ )
{
for (int subs = s & (s - 1); subs; subs = s & (subs - 1))
f[i][s] = min(f[i][s], f[i][subs] + f[i][s ^ subs]);
if (f[i][s] != INF) q.push(mk(f[i][s], i));
}
dij(s);
}
printf("%d\n", f[p[1]][(1 << k) - 1]);
return 0;
}
例题
- 状态表示:设 \(dp(i,S)\) 表示以 \(i\) 为根的一棵树,包含集合 \(S\) 中所有点的最小点权值和。
- 状态转移:$$min(dp(i,S),dp(j,S)+w(j,i))→dp(i,S)$$$$min(dp(i,S),dp(i,T)+dp(i,S−T)-a_i)→dp(i,S) (T⊆S)$$由于此处合并时同一个点 a_i,会被加两次,所以减去。
- 路径记录:
用 \(pre[i][s]\) 记录转移到 \(i\) 为根,连通状态集合为 \(s\) 时的点与集合的信息。在 \(DP\) 结束后从 \(pre[root][S]\) 出发,寻找与集合里的点相连的那些点并逐步分解集合 \(S\),用 \(ans\) 数组来记录被使用的那些点,当集合分解完毕时搜索也就结束了。
code
#define PII pair<int, int>
#define PPI pair<PII, int>
#define x first
#define y second
#define mk make_pair
const int N = 210, M = 2010, INF = 0x3f3f3f3f;
const int mx[4] = {0, 0, 1, -1}, my[4] = {1, -1, 0, 0};
int n, m, K, root;
int f[N][M], a[N], ans[15][15];
bool st[N];
PPI pre[N][M];
queue<PII> q;
bool ins(PII t)
{
return 0 <= t.x && t.x < n && 0 <= t.y && t.y < m;
}
int get(PII t)
{
return t.x * m + t.y;
}
void spfa(int S)
{
memset(st, 0, sizeof st);
while (!q.empty())
{
PII t = q.front();
q.pop();
st[get(t)] = 0;
for (int i = 0; i < 4; i ++ )
{
PII ver = mk(t.x + mx[i], t.y + my[i]);
int gt = get(t), gv = get(ver);
if (ins(ver) && f[gv][S] > f[gt][S] + a[gv])
{
f[gv][S] = f[gt][S] + a[gv];
if (!st[gv])
{
st[gv] = 1;
q.push(ver);
}
pre[gv][S] = mk(t, S);
}
}
}
}
void dfs(PII t, int S)
{
if (!pre[get(t)][S].y) return ;
ans[t.x][t.y] = 1;
int gt = get(t);
if (pre[gt][S].x == t)
dfs(t, S ^ pre[gt][S].y);
dfs(pre[gt][S].x, pre[gt][S].y);
}
int main()
{
memset(f, 0x3f, sizeof f);
rd(n), rd(m);
int tot = 0;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
{
rd(a[tot]);
if (!a[tot])
{
f[tot][1 << (K ++ )] = 0;
root = tot;
}
tot ++ ;
}
for (int S = 1; S < (1 << K); S ++ )
{
for (int i = 0; i < n * m; i ++ )
{
for (int S0 = S & (S - 1); S0; S0 = S & (S0 - 1))
{
if (f[i][S] > f[i][S0] + f[i][S ^ S0] - a[i])
{
f[i][S] = f[i][S0] + f[i][S ^ S0] - a[i];
pre[i][S] = mk(mk(i / m, i % m), S0);
}
}
if (f[i][S] < INF) q.push(mk(i / m, i % m));
}
spfa(S);
}
printf("%d\n", f[root][(1 << K) - 1]);
dfs(mk(root / m, root % m), (1 << K) - 1);
for (int i = 0, tot = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
if (!a[tot ++ ])
putchar('x');
else putchar(ans[i][j] ? 'o' : '_');
}
if (i != n - 1) printf("\n");
}
return 0;
}

浙公网安备 33010602011771号