// // // // // // // // // // // // // //

树形 $dp$ $\&$ 状压 $dp$

树形 \(dp\) & 状压 \(dp\)

树形 \(dp\)

题目

给定一棵树 每个节点都为黑色 将某些节点变为白色 使每条边的两端至少有一个白色 问最少需改变的点

\(f_{u, 0} = \sum f_{v, 1}\)

\(f_{u, 1} = \sum \max \{f_{v, 0}, f_{v, 1}\} + 1\)

树上背包

建树

\(f_{u, j} = \max\{f_{v, k} + f_{u, j - k}\}\)

实际上是背包处理所有儿子 再将这些儿子合并

树上背包的优化

(资料来自网络)

翻到了四种 摘录了两种

背包属于泛化物品 合并两个泛化物品的时间复杂度是 \(O(n^2)\) 的 在树上背包中 就是一个点的每个子树的关系 对于子树的合并 有了树上背包的转移方程

for(int i = head[u]; i; i = e[i].nxt)
{
    int v = e[i].v; if(v == pre) continue;
    dfs(v, u);
    for(int j = m; j >= c[u]; j--)
        for(int k = 0; k <= j - c[u]; k++)
            f[u][j] = Max(f[u][j], f[u][j - k] + f[v][k]);
}

优化:

  1. 改变状态

\(f_{u_s, j}\) 表示以 \(u\) 为根 第 \(s\) 个儿子的所有子节点和前 \(s - 1\) 个儿子构成的背包占用 \(j\) 的容量时最大价值

由于深搜对于树的遍历顺序 一个点的儿子一定先于这个点被处理

在处理每个节点 \(v\) 时 将 \(f_u\) 赋值给 \(f_v\) 此时的 \(f_v\) 表示的就是 \(v\) 之前的所有 \(u\) 的子节点加上原本意义上的 \(f_v\) 其实就是上面的状态

转移 \(f_{u, j} = \max\{f_{v, j - c_v} + val_v \}\)

相当于 \(01\) 背包的转移

for(int i = head[u]; i; i = e[i].nxt)
{
    int v = e[i].v; if(v == pre) continue;
    for(int j = 0; j <= m; j++) f[v][j] = f[u][j];
    dfs(v, u);
	for(int j = c[v]; j <= m; j++) f[u][j] = Max(f[u][j], f[v][j - c[v]] + val[v]);
}
  1. 上下界优化

当在以 \(u\) 为根的子树上选择时 选择的物品最大容量不会超过 \(size_u\) 枚举以子节点 \(v\) 为根的子树全部能选的容量时 在子树中选择的物品最大容量不会超过 \(size_v\)

严格的证明并不会

复杂度均摊 \(O(nm)\)

for(int i = head[u]; i; i = e[i].nxt)
{
    int v = e[i].v; if(v == pre) continue;
    dfs(v, u);
    for(int j = Min(siz[u], m); j; --j)
        for(int k = Min(siz[v], m - j + 1); k; --k)
            f[u][j + k] = Max(f[u][j + k], f[u][j] + f[v][k]);
    siz[u] += siz[v];
}

题目

给定一棵 \(n\) 个点的树 每个点有一个权值 一条边两端至少选一个点 问最大收益

同上面的题目

题目

至多选取 \(k\) 个点

所以多了一个容量 变成了一个背包 多维护一维容量

\(f_{i, j, 0/1}\) 表示 到 \(i\) 点 选择 \(j\) 个点时 是否选该点

\(f_{u, j, 1} = \max \{\max\{f_{v, k, 1}, f_{v, k, 0}\} + f_{u, j - k, 1}\} + v_u\)

\(f_{u, j, 0} = \max \{f_{v, k, 1} + f_{u, j - k, 0}\}\)

强制 \(u\) 点选

(数组中小的一维放到外面可以带来常数上的优化)

优化:

记一个子树大小

浪费时间的背包合并

for(int i = siz[u]; i >= 0; i--)
    for(int j = siz[v]; j >= 0; j--)
    {
        f[u][i + j][0] = Max(f[u][i + j][0], f[u][i][0] + f[v][j][1]);
        f[u][i + j][1] = Max(f[u][i + j][1], f[u][i][1] + Max(f[v][j][0], f[v][j][1]));
    }
siz[u] += siz[v];

复杂度从 \(O(n^3)\) 降到 \(O(n^2)\)

树形背包的上下界优化

树的直径

两遍 \(dfs\)

结论 距离一个点最远的点为树的直径上的一个点

树形 \(dp\)

树的最大独立集

\(f_{u, 0} = \sum \max\{f_{v, 0}, f_{v, 1} \}\)

\(f_{u, 1} = \sum f_{v, 0} + 1\)

\(STA\) \(-\) \(Sration\)

给出一颗 \(n\) 个点的树 找一个点满足该点为根时 所有点的深度之和最大

换根 \(dp\)

/*
  Time: 5.4
  Worker: Blank_space
  Source: P3478 [POI2008]STA-Station
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define ll long long
/*--------------------------------------头文件*/
const int C = 1e6 + 7;
/*------------------------------------常量定义*/
int n, id;
ll f[C], siz[C], sum;
struct edge {int v, nxt;} e[C << 1];
int head[C], ecnt;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
void dfs1(int u, int pre) {
	siz[u] = 1;
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v; if(v == pre) continue;
		dfs1(v, u); siz[u] += siz[v]; f[u] += f[v] + siz[v];
	}
}
void dfs2(int u, int pre) {
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v; if(v == pre) continue;
		f[v] = f[u] + n - 2 * siz[v];
		if(sum < f[v]) sum = f[v], id = v; dfs2(v, u);
	}
}
/*----------------------------------------函数*/
int main() {
	n = read();
	for(int i = 1; i < n; i++)
	{
		int x = read(), y = read();
		add_edge(x, y); add_edge(y, x);
	}
	dfs1(1, 0); dfs2(1, 0); printf("%d", id);
	return 0;
}

\(bzoj3037\) 创世纪

给定一个 \(n\) 个点 \(n\) 条边的图 每个点有一个出边 在图中删去若干点 使所有未删去的点至少有一条入边来自删去的点 求选取的点的最小数量

注意到这是一个基环内向树 题中每个点的出边都为 \(1\) 这是一个基环树森林 考虑反向建边 对于每一个点 该点删 或者是该点至少一个儿子被删

\(f_{i, 0/1}\) 表示到以 \(i\) 点时 以 \(i\) 点为根的子树中 \(i\)\(1\) 删与 \(0\) 不删至少需要保留多少个点

删掉一条边 \(x \to y\) 图变为树 如果 \(x\) 不保留 删掉这条边不影响答案 否则 \(x\) 一定保留且 \(f_{y, 0}\) 初始化为 \(0\)

两次 \(dp\) 取最优

我推的 \(dp\) 式子 :

\(f_{u, 1} = \sum \max\{f_{v, 0}, f_{v, 1}\}\)

\(f_{u, 0} = \sum \max\{f_{v, 0}, f_{v, 1} \} + d + 1\)

\(d = \max\{f_{v, 1} - f_{v, 0} \}\)

状态不太一样 不清楚正确性

给出的式子

\(f_{u, 0} = \sum \max \{f_{v, 0}, f_{v, 1} \}\)

\(f_{u, 1} = \max \sum_{v \ne s} \max\{f_{v, 0}, f_{v, 1} \} + f_{s, 0} + 1\)

贪心解法

从下往上贪 不选叶子 选父亲 最后环上的点选一半

\(Painbow\) \(Road\)

给出一棵树 每条边有颜色 问从哪些点出发不存在一条包含两条同色边的路径

\(dp\) 从每个点向下的子树内是否存在这样的路径

然后再从上往下 \(dp\) 一遍 更新所有点向父节点走是否存在非法路径

注意到若一个点至少有两条相邻的同色边 则同色边指向的子树内的点都不可行

将这样的点标为关键点 这样的边断掉 若最后所有关键点都在一个联通块内则可行 联通块内的所有点都是可行解

\(Hanmiltonian\) \(Spanning\) \(Tree\)

有一个 \(n\) 个点的完全图 每条边的权值为 \(y\) 选择一个生成树 将所有边权改为 \(x\) 求最短的哈密顿回路

哈密顿回路

一个回路通过图中每一个顶点一次 称这个环为哈密顿回路

\(x \geq y\) 特判菊花图 否则一定有一种方案只走 \(y\)

\(x < y\) 则要在树上选尽可能多的边 使得每个点的度数最多为 \(2\) 树形 \(dp\)

void dfs(int u) {
    for(int i = 1; i <= k; i++) dfs(s[i]);
    for(int i = 1; i <= k; i++)
    {
        int g[3];
        g[0] = f[u][0]; g[1] = f[u][1]; g[2] = f[u][2];
        f[u][0] = g[0] + Max(f[s[i]][0], f[s[i]][1], f[s[i]][2]);
        f[u][1] = Max(g[1] + Max(f[s[i]][0], f[s[i]][1], f[s[i]][2]), g[0] + Max(f[s[i]][0], f[s[i]][1]) + 1);
        f[u][2] = Max(g[2] + Max(f[s[i]][0], f[s[i]][1], f[s[i]][2]), g[1] + Max(f[s[i]][0], f[s[i]][1]) + 1);
    }
}
ans = Max(f[1][0], f[1][1], f[1][2]);
/*
  Time: 5.12
  Worker: Blank_space
  Source: CF618D Hamiltonian Spanning Tree
*/
/*--------------------------------------------*/
#include<cstdio>
#define int long long
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, cnt, a, b, d[B << 1], in[B << 1];
struct edge {int v, nxt;} e[B << 2];
int head[B << 1], ecnt;
bool p1;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
bool dfs(int u, int pre, int du = 2) {
	for(int i = head[u], v = e[i].v; i; i = e[i].nxt, v = e[i].v) if(v != pre)
		if(dfs(v, u) && du) du--, cnt++;
	if(du) return 1; else return 0;
}
/*----------------------------------------函数*/
main() {
	n = read(); a = read(); b = read(); if(a >= b) p1 = 1;
	for(int i = 1; i < n; i++)
	{
		int x = read(), y = read(); in[x]++; in[y]++;
		add_edge(x, y); add_edge(y, x);
		if(p1 && (in[x] == n - 1 || in[y] == n - 1)) {printf("%lld", a + (n - 2) * b); return 0;}
	}
	if(p1) {printf("%lld", (n - 1) * b); return 0;}
	dfs(1, 0); printf("%lld", cnt * a + (n - cnt - 1) * b);
	return 0;
}

或者贪心求树的最小路径覆盖

\(Zublicanes\ and\ Mumocrates\)

给出一个 \(n\) 个点的树 保证有偶数个叶子节点 对该树进行黑白染色 使黑色与白色的叶子节点数相等 直接连接两个异色点的边数最少

问题转化为删去一些边使得正好删去一半的叶子

\(f_{i, j}\) 表示当前在 \(i\) 号节点 删去 \(j\) 个叶子至少要删去多少边 树上背包转移

/*
  Time: 5.12
  Worker: Blank_space
  Source: CF581F Zublicanes and Mumocrates
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define Min(x, y) ((x) < (y) ? (x) : (y))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, f[5010][5010][2], out[5010], siz[5010], size, rt = 1;
struct edge {int v, nxt;} e[A];
int head[5010], ecnt;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
void dfs1(int u, int pre) {
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v; out[u]++;
		if(v == pre) continue;
		dfs1(v, u);
	}
	size += out[u] == 1;
}
void dfs2(int u, int pre) {
	if(out[u] == 1) {siz[u] = 1; f[u][0][0] = f[u][1][1] = 0; return ;}
	else f[u][0][0] = f[u][0][1] = 0;
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v; if(v == pre) continue;
		dfs2(v, u); siz[u] += siz[v];
		for(int j = siz[u]; j >= 0; j--)
		{
			int tmp0 = INF, tmp1 = INF;
			for(int k = 0; k <= siz[v]; k++) if(k <= j)
				tmp0 = Min(tmp0, f[u][j - k][0] + Min(f[v][k][0], f[v][k][1] + 1)),
				tmp1 = Min(tmp1, f[u][j - k][1] + Min(f[v][k][1], f[v][k][0] + 1));
			f[u][j][0] = tmp0; f[u][j][1] = tmp1;
		}
	}
}
/*----------------------------------------函数*/
int main() {
	n = read(); memset(f, 63, sizeof f);
	for(int i = 1; i < n; i++)
	{
		int x = read(), y = read();
		add_edge(x, y); add_edge(y, x);
	}
	dfs1(1, 0); while(out[rt] == 1) rt++; dfs2(rt, 0);
	printf("%d", Min(f[rt][size >> 1][0], f[rt][size >> 1][1]));
	return 0;
}

\(treecnt\)

给定一棵 \(n\) 个点的数 从 \(1\)\(n\) 标号 选择 \(k\) 个点 需要选择一些边使得这 \(k\) 个点通过选择的边联通 目标是使得选择的边数最少
计算对于所有选择 \(k\) 个点的情况最小选择边数的总和为多少

枚举每条边 若这条边两侧都有点被选中 对答案贡献 \(1\) 统计情况时补集转化 求全部 \(k\) 个点都在某一边时的情况数 计算组合数

预处理阶乘和组合数要用的逆元

深搜对于每个点统计答案

/*
  Time: 5.4
  Worker: Blank_space
  Source: treecnt
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define int long long
#define Abs(x) ((x) < 0 ? -(x) : (x))
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, k, f[B], siz[B], ans;
struct edge {int v, nxt;} e[B << 1];
int head[B], ecnt;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
int power(int a, int b, int res = 1) {for(; b; a = a * a % mod, b >>= 1) if(b & 1) res = res * a % mod; return res;}
int C(int n, int m) {return n < m ? 0 : f[n] * power(f[m], mod - 2) % mod * power(f[n - m], mod - 2) % mod;}
void dfs(int u, int pre) {
	siz[u] = 1;
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v; if(v == pre) continue;
		dfs(v, u); siz[u] += siz[v];
		ans = ((ans + C(n, k) - C(siz[v], k) - C(n - siz[v], k)) % mod + mod) % mod;
	}
}
/*----------------------------------------函数*/
signed main() {
	n = read(); k = read(); f[0] = f[1] = 1;
	for(int i = 2; i <= n; i++) f[i] = f[i - 1] * i % mod;
	for(int i = 1; i < n; i++)
	{
		int x = read(), y = read();
		add_edge(x, y); add_edge(y, x);
	}
	dfs(1, 0); printf("%lld", ans);
	return 0;
}

黑暗料理

给定 \(n\) 个节点的树 有 \(m\) 个特殊位置 任选位置出发与结束 经过 \(m\) 个点中随机给定的 \(k\) 个点的最短路程

求期望经过随机 \(k\) 个点要走的距离 答案对 \(998244353\) 取模

...

小凸玩密室

状压 \(dp\)

旅行商问题

旅行商要到若干城市旅行 各城市之间的费用是已知的 为了节省费用 旅行商决定从所在城市出发 到每个城市旅行一次后返回初始城市 问他选择什么样的旅行才能使所走的总费用最短

状态定义 \(f_{i, S}\) 表示经过点的集合为 \(S\) 当前到达 \(i\) 号节点的最小花费

\(S\) 通过二进制存储 访问过的记 \(1\) 未访问过的记 \(0\)

int f[20][(1 << 17) + 5];
for(int i = 1; i <= n; i++)
{
    memset(f, 63, sizeof f);
    f[i][0] = 0;
    for(int s = 0; s < (1 << n); s++)
    {
        for(int j = 1; j <= n; j++) if(f[j][s] != INF)
        {
            for(int k = 1; k <= n; k++) if(!(s >> k - 1) & 1)
            {
                f[k][s | (1 << k - 1)] = Min(f[k][s | (1 << k - 1)], f[j][s] + a[j][k]);
            }
        }
	}
    ans = Min(ans, f[i][(1 << n) - 1]);
}

\(Little\ Pony\ and\ Harmony\ Chest\)

注意到 \(n \leq 100\ a_i \leq 30\)

当一个 \(b_i\) 超过 \(60\) 时 选择 \(1\) 一定更优 所以一共只有 \(17\) 个质数 考虑状压

\(f_{i, S}\) 表示当前填到第 \(i\)\(b\) 中的数 质数的出现状态是 \(S\) 枚举当前填的数是什么

/*
  Time: 5.5
  Worker: Blank_space
  Source: CF453B Little Pony and Harmony Chest
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define Abs(x) ((x) < 0 ? -(x) : (x))
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, f[110][(1 << 16) + 5], a[110], s[60], g[110][(1 << 16) + 5], min = INF, _s, ans[110];
int p[20] = {0, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53};
bool vis[60];
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void dfs(int i, int c) {
	if(!i) return ; dfs(i - 1, c ^ s[g[i][c]]);
	printf("%d ", g[i][c]);
}
/*----------------------------------------函数*/
int main() {
	n = read(); memset(f, 63, sizeof f); f[0][0] = 0;
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1; i <= 58; i++) for(int j = 1, k = 1; j <= 16; j++, k <<= 1)
		if(!(i % p[j])) s[i] |= k;
	for(int i = 1; i <= n; i++) for(int k = 1; k <= 58; k++)
	{
		int S = (1 << 16) - 1 ^ s[k];
		for(int b = S; b >= 0; b = b - 1 & S)
		{
			if(f[i][b | s[k]] > f[i - 1][b] + Abs(a[i] - k))
				f[i][b | s[k]] = f[i - 1][b] + Abs(a[i] - k), g[i][b | s[k]] = k;
			if(!b) break;
		}
	}
	for(int b = 0; b < 1 << 16; b++) if(f[n][b] < min) min = f[n][b], _s = b;
	for(int i = n; i; --i) ans[i] = g[i][_s], _s ^= s[g[i][_s]];
	for(int i = 1; i <= n; i++) printf("%d ", ans[i]);
	return 0;
}

\([CDOJ\ 1296]\)

\(Corn\ Fields\ G\)

状压板子

/*
  Time: 5.5
  Worker: Blank_space
  Source: P1879 [USACO06NOV]Corn Fields G
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#define Abs(x) ((x) < 0 ? -(x) : (x))
#define Max(x, y) ((x) > (y) ? (x) : (y))
#define Min(x, y) ((x) < (y) ? (x) : (y))
#define Swap(x, y) ((x) ^= (y) ^= (x) ^= (y))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e8;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, m, f[15][400], s[400], cnt, mp[15], ans;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/

/*----------------------------------------函数*/
int main() {
	n = read(); m = read();
	for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) if(!read()) mp[i] |= 1 << j - 1;
	for(int i = 0; i < 1 << m; i++) if(!(i & (i << 1))) s[++cnt] = i;
	for(int i = 1; i <= cnt; i++) if(!(mp[1] & s[i])) f[1][i] = 1;
	for(int i = 2; i <= n; i++) for(int j = 1; j <= cnt; j++) if(!(s[j] & mp[i]))
		for(int k = 1; k <= cnt; k++) if(!(s[k] & s[j]) && !(s[k] & mp[i - 1]))
			f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
	for(int i = 1; i <= cnt; i++) ans = (ans + f[n][i]) % mod;
	printf("%d", ans);
	return 0;
}

游览计划

给出一个 \(n \times m\) 的矩阵 其中 \(k\) 个格子是景点 选一些非景点的格子作为游览路径 使景点联通 将不同格子选为路径需要聘用不同数量的志愿者 求需聘请多少志愿者

一类问题 最小斯坦纳树问题

给出一个 \(n\) 个点 \(m\) 条边的无向带权图 指定 \(k\) 个点 问让 \(k\) 个点联通的边集的最小和是多少

\(f_{i, S}\) 表示当前在 \(i\) 号点 且把指定点集的子集 \(S\) 全部联通的最小边权和

转移

\[f_{i, S} = \begin{cases}f_{i, S'} + f_{i, S - S'} \\ f_{j, S'} + dis_{i, j} \end{cases} \]

第二种转移可能出现环 最短路处理
复杂度 \(O(n3^k + m2^k)\)
对于游览计划 记录转移顺序以便输出方案

/*
  Time: 5.8
  Worker: Blank_space
  Source: P4294 [WC2008]游览计划
*/
/*--------------------------------------------*/
#include<cstdio>
#include<cstring>
#include<queue>
#define Min(x, y) ((x) < (y) ? (x) : (y))
/*--------------------------------------头文件*/
const int A = 1e4 + 7;
const int B = 1e5 + 7;
const int C = 1e6 + 7;
const int D = 1e7 + 7;
const int mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
/*------------------------------------常量定义*/
int n, m, f[110][(1 << 10) + 5], a[110], p[15], cnt, pre[110][(1 << 10) + 5];
struct edge {int v, nxt;} e[510];
int head[110], ecnt;
bool vis[110], _vis[110][(1 << 10) + 5], mp[110];
std::priority_queue <std::pair <int, int> > q;
/*------------------------------------变量定义*/
inline int read() {
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar();}
	return x * f;
}
/*----------------------------------------快读*/
void add_edge(int u, int v) {e[++ecnt] = (edge){v, head[u]}; head[u] = ecnt;}
int d(int x, int y) {return (x - 1) * m + y;}
void dijk(int s) {
	memset(vis, 0, sizeof vis);
	while(!q.empty())
	{
		int u = q.top().second; q.pop();
		if(vis[u]) continue; vis[u] = 1;
		for(int i = head[u], v = e[i].v; i; i = e[i].nxt, v = e[i].v)
			if(f[v][s] > f[u][s] + a[v]) f[v][s] = f[u][s] + a[v], q.push(std::make_pair(-f[v][s], v)), pre[v][s] = u;
	}
}
void dfs(int u, int S) {
	if(_vis[u][S]) return ; _vis[u][S] = 1; mp[u] = 1;
	if(pre[u][S] && f[pre[u][S]][S] + a[u] == f[u][S]) {int t = pre[u][S]; while(t) dfs(t, S), t = pre[t][S];}
	for(int s = S & S - 1; s; s = S & s - 1) if(f[u][S] == f[u][s] + f[u][S ^ s] - a[u]) {dfs(u, s); dfs(u, s ^ S); break;}
}
/*----------------------------------------函数*/
int main() {
	n = read(); m = read(); memset(f, 63, sizeof f);
	for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++)
	{
		a[d(i, j)] = read();
		if(i > 1) add_edge(d(i, j), d(i - 1, j)), add_edge(d(i - 1, j), d(i, j));
		if(j > 1) add_edge(d(i, j), d(i, j - 1)), add_edge(d(i, j - 1), d(i, j));
		if(!a[d(i, j)]) p[++cnt] = d(i, j), f[p[cnt]][1 << cnt - 1] = 0;
	}
	for(int S = 1; S < 1 << cnt; dijk(S), S++) for(int i = 1; i <= n * m; i++)
	{
		for(int s = S & S - 1; s; s = S & s - 1) f[i][S] = Min(f[i][S], f[i][s] + f[i][S ^ s] - a[i]);
		if(f[i][S] != INF) q.push(std::make_pair(-f[i][S], i));
	}
	printf("%d\n", f[p[1]][(1 << cnt) - 1]); dfs(p[1], (1 << cnt) - 1);
	for(int i = 1; i <= n; i++, puts("")) for(int j = 1; j <= m; j++)
	{
		if(a[d(i, j)] && mp[d(i, j)]) printf("o");
		if(a[d(i, j)] && !mp[d(i, j)]) printf("_");
		if(!a[d(i, j)]) printf("x");
	}
	return 0;
}

\(——\ End\)

posted @ 2021-05-04 22:27  Blank_space  阅读(107)  评论(0)    收藏  举报
// // // // // // //