1272 lines
36 KiB
Markdown
1272 lines
36 KiB
Markdown
# 动态规划
|
||
|
||
- [动态规划](#动态规划)
|
||
- [背包](#背包)
|
||
- [01背包](#01背包)
|
||
- [完全背包](#完全背包)
|
||
- [多重背包](#多重背包)
|
||
- [混合背包](#混合背包)
|
||
- [分组背包](#分组背包)
|
||
- [有依赖的背包](#有依赖的背包)
|
||
- [背包问题的第 $k$ 优解](#背包问题的第-k-优解)
|
||
- [背包例题](#背包例题)
|
||
- [P4141 消失之物](#p4141-消失之物)
|
||
- [P4141题解](#p4141题解)
|
||
- [P4141代码](#p4141代码)
|
||
- [区间](#区间)
|
||
- [区间 dp 例题](#区间-dp-例题)
|
||
- [P1775 石子合并](#p1775-石子合并)
|
||
- [P1775题解](#p1775题解)
|
||
- [P1175变式(P1880)](#p1175变式p1880)
|
||
- [P1880法1](#p1880法1)
|
||
- [P1880法2](#p1880法2)
|
||
- [P1880代码](#p1880代码)
|
||
- [P10236 D. 排卡](#p10236-d-排卡)
|
||
- [P10236题解](#p10236题解)
|
||
- [P10236代码](#p10236代码)
|
||
- [P1450 硬币购物](#p1450-硬币购物)
|
||
- [P1450题解](#p1450题解)
|
||
- [P1450代码](#p1450代码)
|
||
- [P5365 英雄联盟](#p5365-英雄联盟)
|
||
- [P5365题解](#p5365题解)
|
||
- [P5365代码](#p5365代码)
|
||
- [LOJ3254 集邮比赛 3](#loj3254-集邮比赛-3)
|
||
- [LOJ3254题解](#loj3254题解)
|
||
- [LOJ3254代码](#loj3254代码)
|
||
- [树形](#树形)
|
||
- [选择节点类树形 dp](#选择节点类树形-dp)
|
||
- [树形背包类树形 dp](#树形背包类树形-dp)
|
||
- [树形 dp 例题](#树形-dp-例题)
|
||
- [树上最小点覆盖](#树上最小点覆盖)
|
||
- [树上最小点覆盖题解](#树上最小点覆盖题解)
|
||
- [P2014 选课](#p2014-选课)
|
||
- [P2014题解](#p2014题解)
|
||
- [P1352 没有上司的舞会](#p1352-没有上司的舞会)
|
||
- [P1352题解](#p1352题解)
|
||
- [P1352代码](#p1352代码)
|
||
- [P2899 Cell Phone Network G](#p2899-cell-phone-network-g)
|
||
- [P2899题解](#p2899题解)
|
||
- [P2899代码](#p2899代码)
|
||
- [换根](#换根)
|
||
- [换根 dp 例题](#换根-dp-例题)
|
||
- [P3478 STA-Station](#p3478-sta-station)
|
||
- [P3478题解](#p3478题解)
|
||
- [P3478代码](#p3478代码)
|
||
- [CF543D Road Improvement](#cf543d-road-improvement)
|
||
- [CF534D题解](#cf534d题解)
|
||
- [CF534D代码](#cf534d代码)
|
||
- [状压](#状压)
|
||
- [引入例题 旅行商问题](#引入例题-旅行商问题)
|
||
- [旅行商问题题解](#旅行商问题题解)
|
||
- [常见技巧](#常见技巧)
|
||
- [位运算操作](#位运算操作)
|
||
- [popcount](#popcount)
|
||
- [状压 dp 例题](#状压-dp-例题)
|
||
- [P2831 愤怒的小鸟](#p2831-愤怒的小鸟)
|
||
- [P2831题解](#p2831题解)
|
||
- [P2831代码](#p2831代码)
|
||
- [P1896 互不侵犯](#p1896-互不侵犯)
|
||
- [P1896题解](#p1896题解)
|
||
- [P1896代码](#p1896代码)
|
||
- [P4460 解锁屏幕](#p4460-解锁屏幕)
|
||
- [P4460题解](#p4460题解)
|
||
- [P4460代码](#p4460代码)
|
||
- [P6239 奇怪的道路](#p6239-奇怪的道路)
|
||
- [P6239题解](#p6239题解)
|
||
- [P6239代码](#p6239代码)
|
||
|
||
## 背包
|
||
|
||
### 01背包
|
||
|
||
给定 $n$ 个物品,第 $i$ 个的体积为 $v_i$,价值为 $w_i$,每个物品至多选一次,求最大价值。
|
||
|
||
转移方程 $f(i, j) = max(f(i - 1, j), f(i - 1, j - v_i) + w_i)$
|
||
|
||
时间复杂度 $O(nm)$
|
||
|
||
### 完全背包
|
||
|
||
给定 $n$ 个物品,第 $i$ 个的体积为 $v_i$,价值为 $w_i$,每个物品无限拿,求最大价值。
|
||
|
||
转移方程 $f(i, j) = max(f(i - 1, j), f(i, j - v_i) + w_i)$
|
||
|
||
时间复杂度 $O(nm)$
|
||
|
||
### 多重背包
|
||
|
||
给定 $n$ 个物品,第 $i$ 个的体积为 $v_i$,价值为 $w_i$,每个物品有 $t_i$ 个,求最大价值。
|
||
|
||
考虑 $t_i$ 的二进制,若第 $k$ 位为 $1$,则新建一个体积为 $v_i 2^k$,价值为 $w_i 2^k$ 的物品,转换为[01背包](#01背包)。
|
||
|
||
时间复杂度 $O(nm \log m)$
|
||
|
||
### 混合背包
|
||
|
||
就是将[01背包](#01背包),[完全背包](#完全背包),[多重背包](#多重背包)混合在一起。
|
||
|
||
因为 dp 数组并没有规定是哪种背包,所以共用一个数组, 分三类做即可。时间复杂度 $O(nm)$
|
||
|
||
### 分组背包
|
||
|
||
有 $n$ 个物品,第 $i$ 个物品价值为 $w_i$ 体积为 $v_i$ 并且属于第 $t_i$ 组。
|
||
|
||
有一个容量为 $m$ 的背包,问在每组最多选一个且体积不超过 $W$ 的条件下的最大价值。
|
||
|
||
一组一组进行考虑。对于每一组,从大到小枚举 $j$ 表示这一组的体积,然后枚举每一个物品即可。
|
||
|
||
核心代码:
|
||
|
||
```cpp
|
||
for (int i = 1; i <= m; ++i)
|
||
{
|
||
for (int j = W; j >= 0; --j)
|
||
{
|
||
for (auto x : items[i])
|
||
{
|
||
if (j >= v[x])
|
||
f[j] = max(f[j], f[j - v[x]] + w[x]);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 有依赖的背包
|
||
|
||
金明有 $n$ 元钱, $m$ 个物品,第 $i$ 件物品的价格为 $v_i$,重要度为 $p_i$。有些物品是主件,可以直接购买;有些物品是附件,要先买下主件才能买附件。
|
||
|
||
求最大的 $v_i \times p_i$
|
||
|
||
$n \le 3.2 \times 10^4, m \le 60, 0 \le v_i \le 10^4, 1 \le p_i \le 5$。每个主件最多有两个附件。
|
||
|
||
对于每个主件 $A$ 以及附件 $B, C$,只有这几种选法: $\varnothing, \{A\}, \{A, B\}, \{A, C\}, \{A, B, C\}$
|
||
|
||
考虑将其转换为每组 $4$ 个物品的[分组背包](#分组背包)求解。
|
||
|
||
### 背包问题的第 $k$ 优解
|
||
|
||
普通01背包只能求出最优解,在普通背包上稍作改动,增加一维用于记录当前状态前 $k$ 优解,即可求出背包问题的第 $k$ 优解。
|
||
|
||
具体的,考虑原本的转移: $f_{i, j} = max(f_{i - 1, j}, f_{i - 1, j - v_i} + w_i)$
|
||
|
||
所有不选 $i$ 的构成序列 $[f_{i - 1, j, 1}, ...,f_{i - 1, j, k}]$
|
||
|
||
所有选 $i$ 的构成序列 $[f_{i - 1, j - v_i, 1} + w_i, ...,f_{i - 1, j - v_i, k} + w_i]$
|
||
|
||
合并后即得到 $f_{i, j, k}$
|
||
|
||
时间复杂度 $O(nmk)$
|
||
|
||
### 背包例题
|
||
|
||
#### P4141 消失之物
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P4141)
|
||
|
||
有 $n$ 个物品。体积分别是 $w_1, w_2, \dots, w_n$,每个只有一个。
|
||
|
||
记 $cnt(i, x)$ 表示在丢掉第 $i$ 件物品的情况下装满容量为 $x$ 的背包的方案数。
|
||
|
||
输出一个 $n \times m$ 的矩阵,表示每一个 $cnt(i, x)$ 的个位数。( $m$ 为输入的背包容量最大值)
|
||
|
||
##### P4141题解
|
||
|
||
先按照不丢东西的方法跑一次背包。
|
||
|
||
然后考虑丢掉一件物品后造成的影响。
|
||
|
||
因为本题目要求的是方案数而不是最优值,所以每一个物品的贡献都可以撤销。
|
||
|
||
考虑转移方程
|
||
|
||
$$
|
||
f_j = g_j + g_{j - w_i}\\
|
||
\Downarrow\\
|
||
g_j = f_j - g_{j - w_i}
|
||
$$
|
||
|
||
因为需要用到更小的 $g_{j - w_i}$,所以从小到大循环即可逆推出 $g_i$。
|
||
|
||
##### P4141代码
|
||
|
||
```cpp
|
||
#pragma GCC optimize(3, "Ofast", "inline")
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
const int N = 2e5 + 10;
|
||
int n, m, w[N], f[N], g[N];
|
||
int main()
|
||
{
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("1.in", "r", stdin);
|
||
#endif
|
||
scanf("%d%d", &n, &m);
|
||
for (int i = 1; i <= n; ++i)
|
||
scanf("%d", &w[i]);
|
||
f[0] = 1;
|
||
for (int i = 1; i <= n; ++i)
|
||
for (int j = m; j >= w[i]; --j)
|
||
f[j] = (f[j] + f[j - w[i]]) % 10;
|
||
for (int i = 1; i <= n; ++i)
|
||
{
|
||
memset(g, 0, sizeof(g));
|
||
g[0] = 1;
|
||
for (int j = 1; j <= m; ++j)
|
||
{
|
||
if (j - w[i] >= 0)
|
||
g[j] = (f[j] - g[j - w[i]] + 10) % 10;
|
||
else
|
||
g[j] = f[j] % 10;
|
||
printf("%d", g[j] % 10);
|
||
}
|
||
puts("");
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## 区间
|
||
|
||
区间dp是一种特殊的dp方式,他以每个子区间为一个状态,一般以 $f_{l, r}$ 表示 $[l, r]$ 的状态。
|
||
|
||
### 区间 dp 例题
|
||
|
||
#### P1775 石子合并
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P1775)
|
||
|
||
在一条路上有 $n$ 个棋子,第 $i$ 堆有 $a_i$ 枚,每次操作可以合并相邻的两堆棋子,能获得的得分为两堆棋子的总和。问最大总得分。
|
||
|
||
$n \le 300$
|
||
|
||
##### P1775题解
|
||
|
||
设 $f_{l, r}$ 表示 $[l, r]$ 这个区间的答案。
|
||
|
||
转移方程式则为
|
||
|
||
$$
|
||
f_{l, r} = \mathop{max}\limits_{i \le k \lt j}\{f_{i, k} + f_{k + 1, j}+\sum^{j}_{t = i} a_t\}
|
||
$$
|
||
|
||
这题代码就不放了,重点是下面的变式。
|
||
|
||
##### P1175变式(P1880)
|
||
|
||
若石子摆成一个环而非一条链,如何解决?
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P1880)
|
||
|
||
###### P1880法1
|
||
|
||
做 $n$ 次,时间复杂度 $O(n^4)$
|
||
|
||
###### P1880法2
|
||
|
||
将序列循环一次拼接到后边,再去执行原来的 dp,最后答案则为
|
||
|
||
$$
|
||
max\{f_{1, n}, f_{2, n + 1}, \dots, f_{n, 2\times n - 1}\}
|
||
$$
|
||
|
||
复杂度 $O(n^3)$
|
||
|
||
###### P1880代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
|
||
int n;
|
||
int a[500];
|
||
|
||
int f[500][500] = {0};
|
||
int fm[500][500] = {0};
|
||
int pre[500] = {0};
|
||
int ans_max = -1;
|
||
int ans_min = 0x7f7f7f7f;
|
||
|
||
int max(int a, int b)
|
||
{
|
||
if (a > b)
|
||
return a;
|
||
return b;
|
||
}
|
||
|
||
int min(int a, int b)
|
||
{
|
||
if (a < b)
|
||
return a;
|
||
return b;
|
||
}
|
||
|
||
int sum(int i, int j)
|
||
{
|
||
return pre[j] - pre[i - 1];
|
||
}
|
||
|
||
int main()
|
||
{
|
||
int i, j, k;
|
||
scanf("%d", &n);
|
||
for (i = 1; i <= n; i++)
|
||
{
|
||
scanf("%d", &a[i]);
|
||
a[n + i] = a[i];
|
||
}
|
||
|
||
int len = (n << 1) - 1;
|
||
for (i = 1; i <= len; i++)
|
||
{
|
||
pre[i] = pre[i - 1] + a[i];
|
||
}
|
||
|
||
memset(fm, 0x7f, sizeof(fm));
|
||
for (i = 0; i <= 400; i++)
|
||
fm[i][i] = 0;
|
||
for (i = 2; i <= n; i++)
|
||
{
|
||
for (j = 1; j <= len - i + 1; j++)
|
||
{
|
||
for (k = j; k < j + i - 1; k++)
|
||
{
|
||
int m = f[j][k] + f[k + 1][j + i - 1];
|
||
|
||
int xiao = fm[j][k] + fm[k + 1][j + i - 1];
|
||
|
||
if (f[j][j + i - 1] < m)
|
||
f[j][j + i - 1] = m;
|
||
|
||
if (fm[j][j + i - 1] > xiao)
|
||
fm[j][j + i - 1] = xiao;
|
||
}
|
||
f[j][j + i - 1] += sum(j, j + i - 1);
|
||
fm[j][j + i - 1] += sum(j, j + i - 1);
|
||
}
|
||
}
|
||
for (i = 1; i <= n; i++)
|
||
{
|
||
if (ans_max < f[i][i + n - 1])
|
||
ans_max = f[i][i + n - 1];
|
||
|
||
if (ans_min > fm[i][i + n - 1])
|
||
ans_min = fm[i][i + n - 1];
|
||
}
|
||
printf("%d\n", ans_min);
|
||
printf("%d", ans_max);
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P10236 D. 排卡
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P10236)
|
||
|
||
给定一个长度为 $n$ 的双端队列 $q$,进行 $n$ 次操作,第 $i$ 次操作令 $b_i \leftarrow q$ 的队头或队尾,并弹出那个元素。
|
||
|
||
要求构造操作方式,使得
|
||
|
||
$$
|
||
\sum^{n - 1}_{i = 1}pow(b_i, b_{i + 1}, 998244353)
|
||
$$
|
||
|
||
最大。
|
||
|
||
其中 $pow(a, b, p) = a^b \bmod p$
|
||
|
||
注意:只对于每次计算取模,后续加起来时不取模。
|
||
|
||
$n \le 10^3$
|
||
|
||
##### P10236题解
|
||
|
||
考虑使用区间 dp,使用 $f_{l, r, b}$ 表示 $b = 0$ 时,访问的数是左边, $b = 1$ 则表示右边, $[l, r]$ 区间内的答案。则转移方程为:
|
||
|
||
$$
|
||
f_{i + 1, j, 0} = max(f_{i + 1, j, 0}, f_{i, j, 0} + a^{a_i}_{i - 1}, f(i, j, 1) + a^{a_i}_{j + 1}) \\
|
||
f_{i, j - 1, 1} = max(f_{i, j - 1, 1}, f_{i, j, 1} + a^{a_i}_{j + `}, f(i, j, 0) + a^{a_i}_{i - 1}) \\
|
||
$$
|
||
|
||
时间复杂度为 $O(n^2)$
|
||
|
||
##### P10236代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
#define int long long
|
||
int t, n, a[1005], f[1005][1005][2], mod = 998244353;
|
||
int fast_pow(int a, int b, int c = mod)
|
||
{
|
||
if (a == 0 && b == 0)
|
||
return 0;
|
||
int x, y, m = 1;
|
||
y = a, x = b;
|
||
while (b)
|
||
{
|
||
if (b % 2)
|
||
m = m * a % c;
|
||
a = a * a % c;
|
||
b /= 2;
|
||
}
|
||
return m;
|
||
}
|
||
|
||
signed main()
|
||
{
|
||
scanf("%lld", &t);
|
||
while (t--)
|
||
{
|
||
scanf("%lld", &n);
|
||
for (int i = 1; i <= n; i++)
|
||
scanf("%lld", &a[i]);
|
||
memset(f, 0, sizeof(f));
|
||
//-----------------dp-------------------//
|
||
for (int len = 1; len < n; len++)
|
||
for (int i = 1; i + len <= n; i++)
|
||
{
|
||
f[i][i + len][0] = max(f[i + 1][i + len][0] + fast_pow(a[i], a[i + 1]), f[i + 1][i + len][1] + fast_pow(a[i], a[i + len])); // left
|
||
f[i][i + len][1] = max(f[i][i + len - 1][0] + fast_pow(a[i + len], a[i]), f[i][i + len - 1][1] + fast_pow(a[i + len], a[i + len - 1])); // right
|
||
}
|
||
printf("%lld\n", max(f[1][n][0], f[1][n][1]));
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P1450 硬币购物
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P1450)
|
||
|
||
共有 $4$ 种硬币。面值分别为 $c_1, c_2, c_3, c_4$。
|
||
|
||
某人去商店买东西,去了 $n$ 次,对于每次购买,他带了 $d_i$ 枚 $i$ 种硬币,想购买 $s$ 的价值的东西。请问每次有多少种付款方法。
|
||
|
||
$1 \le n \le 1000$
|
||
|
||
##### P1450题解
|
||
|
||
若直接套用多重背包,肯定爆时间。
|
||
|
||
##### P1450代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
unsigned long long c[5];
|
||
int n;
|
||
unsigned long long d[5];
|
||
unsigned int t;
|
||
int main()
|
||
{
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("3.in", "r", stdin);
|
||
#endif
|
||
scanf("%llu%llu%llu%llu%d", &c[0], &c[1], &c[2], &c[3], &n);
|
||
while (n--)
|
||
{
|
||
scanf("%llu%llu%llu%llu%u", &d[0], &d[1], &d[2], &d[3], &t);
|
||
vector<unsigned long long> f(t + 1);
|
||
f[0] = 1;
|
||
for (size_t o = 0; o < 4; ++o)
|
||
{
|
||
unsigned int w = c[o], m = d[o];
|
||
vector<unsigned long long> g(f);
|
||
vector<unsigned long long> s(w);
|
||
for (unsigned long long i = 0; i * w <= t; ++i)
|
||
for (unsigned long long j = 0; j < w && i * w + j <= t; ++j)
|
||
{
|
||
g[i * w + j] += s[j];
|
||
s[j] += f[i * w + j];
|
||
if (i >= m)
|
||
s[j] -= f[(i - m) * w + j];
|
||
}
|
||
f = g;
|
||
}
|
||
printf("%llu\n", f.back());
|
||
}
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P5365 英雄联盟
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P5365)
|
||
|
||
有 $n$ 个英雄,每个英雄有 $k_i$ 个皮肤i,一个英雄所有的皮肤价格相同,都是 $c_i$,现在要买皮肤来进行展示,展示的方案就是每个英雄选择一个皮肤展示,求如何花最少的前使得总的展示方案 $\le m$。
|
||
|
||
$n \le 130, m \le 10^{17}, k_i \le 10, c_i \le 200$
|
||
|
||
##### P5365题解
|
||
|
||
朴素方案是 $f_{i, j}$ 表示前 $i$ 个物品,方案数至少为 $j$ 的最小花费,但是观察到 $m \le 10^17$,方案数过多,肯定存不下。
|
||
|
||
于是考虑使用 $f_{i, j}$ 表示前 $i$ 个物品,价格最大为 $j$ 的最大方案数,就转换为了一个多重背包,转移为:
|
||
|
||
$$
|
||
f_{i + 1, j + c_i \times t} = max \{f(i, j)\times t\}
|
||
$$
|
||
|
||
其中 $t$ 表示买了 $t$ 个皮肤。
|
||
|
||
时间复杂度 $O(n^2kc)$
|
||
|
||
- 注意:为了防止溢出,方案数要与 $m$ 取 min
|
||
|
||
##### P5365代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
// fi = max(fi, f[i - g * c[p]] * g)
|
||
#define int long long
|
||
const int N = 1000005;
|
||
using namespace std;
|
||
int f[N], k[N], c[N], n, m, sq, ans;
|
||
signed main()
|
||
{
|
||
f[0] = 1;
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("4.in", "r", stdin);
|
||
#endif
|
||
scanf("%lld%lld", &n, &m);
|
||
for (int p = 1; p <= n; ++p)
|
||
scanf("%lld", &k[p]);
|
||
for (int p = 1; p <= n; ++p)
|
||
{
|
||
scanf("%d", &c[p]);
|
||
sq += k[p] * c[p];
|
||
}
|
||
for (int p = 1; p <= n; ++p)
|
||
for (int i = sq; i >= 0; --i)
|
||
for (int g = 0; g <= k[p] && g * c[p] <= i; ++g)
|
||
f[i] = max(f[i], f[i - g * c[p]] * g);
|
||
while (ans < sq && f[ans] < m)
|
||
++ans;
|
||
printf("%d\n", ans);
|
||
}
|
||
```
|
||
|
||
#### LOJ3254 集邮比赛 3
|
||
|
||
[题目链接](https://loj.ac/p/3254)
|
||
|
||
JOI 君生活的 IOI 国有一个著名的湖泊,今天一场集邮大会在湖边举行。
|
||
|
||
绕湖一圈总共有 $N$ 种邮票可以收集,编号分别为 $1\ldots N$,收集点绕湖顺时针排列。湖的周长为 $L$,第 $i$ 张邮票 ($1 \le i \le N$) 的收集点在距离出发点顺时针走 $X_i$ 米的位置。
|
||
|
||
参赛者在比赛开始的时候要站在出发点的位置,当大会开始时,参赛者可以绕湖顺时针或者逆时针移动,参赛者能够得到第 $i$ 张邮票 ($1 \le i \le N$) 当且仅当他到达收集点的时间在比赛开始时的 $T_i$ 秒以内(含)。
|
||
|
||
JOI 君也是集邮大会的参与者。他的移动速度是每秒钟 $1$ 米,你可以认为只有移动才会消耗时间。
|
||
|
||
计算他最多能收集到多少种邮票。
|
||
|
||
$n \le 200, 2 \le L \le 10^9, 0 \le T_i \le 10^9$
|
||
|
||
##### LOJ3254题解
|
||
|
||
##### LOJ3254代码
|
||
|
||
---
|
||
|
||
## 树形
|
||
|
||
树形 dp 本质上只是一种思想,就是把 dp 建立在树形结构上。树形 dp 最主要的特征就是考虑树上的父子关系,例如选择一个节点必须选择或不能选择父亲等。
|
||
|
||
常见的树形 dp 有两种模型:
|
||
|
||
- 选择节点类,转移方程类似 $f_{i, 0} = f_{j, 1}, f_{i, 1} = max(f_{j, 0}, f_{j, 1})$,主要是处理树上某个节点是否选择。
|
||
- 树形背包类,转移方程类似 $f_{v, k} = f_{u, k} + val, f_{u, k} = max(f_{v, k}, f_{v, k - 1})$,主要是处理树上背包问题。
|
||
|
||
### 选择节点类树形 dp
|
||
|
||
这一类的 dp 主要是处理某一个节点是否对答案产生贡献,即是要选择该答案到答案集合中取。所以在设计转移方程 $f_{i, 0/1}$ 时,第二维则表示该节点是否选取。
|
||
|
||
### 树形背包类树形 dp
|
||
|
||
该类 dp,通常是在树上选择一个包含根的连通块,或者背包存在依赖关系,或者需要知道每个点的子树选了多少。
|
||
|
||
### 树形 dp 例题
|
||
|
||
#### 树上最小点覆盖
|
||
|
||
给定一个 $n$ 个节点的树,要求选择一个点的集合,使得树上每一条边都与其中至少一个点相连,同时让集合大小最小。
|
||
|
||
$n \le 10^6$
|
||
|
||
##### 树上最小点覆盖题解
|
||
|
||
使用状态 $f_{i, 0/1}$ 表示节点 $i$ 选择或不选择。
|
||
|
||
考虑 DFS 进行状态转移,对于节点 $u$,以及其子节点集合 $V_s$,有如下转移:
|
||
|
||
$$
|
||
\begin{aligned}
|
||
f_{u, 1} &= \sum_{v \in V_s} min(f_{v, 0}, f_{v, 1}) \\
|
||
f_{u, 0} &= \sum_{v \in V_s} f_{v, 1} \\
|
||
\end{aligned}
|
||
$$
|
||
|
||
#### P2014 选课
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P2014)
|
||
|
||
有 $n$ 个学科,每门课有个学分,每门课程有一门或者没有先修课(若课程 $a$ 是课程 $b$ 的先修课,则必须先修课程 $a$ 在能学习课程 $b$)。一个学生要从这些课程内选择 $m$ 门课程进行学习,问他能获得的最大学分。
|
||
|
||
$1 \le n, m \le 300$
|
||
|
||
##### P2014题解
|
||
|
||
考虑构造树形关系,在 dfs 时转移。记 $f_{u, i, j}$ 表示 $u$ 为根的子树中,已经遍历了 $i$ 课子树,选择了 $j$ 门课的最大学分。转移方程如下:
|
||
|
||
$$
|
||
f_{u, i, j} = \mathop{max}_{k \le min(j, sz_v)}(f_{u, i - 1, j - k} + f_{v, s_v, k})
|
||
$$
|
||
|
||
其中 $s_v$ 表示 $v$ 的子节点数量, $sz_v$ 表示以 $v$ 为根的树的节点数量。
|
||
|
||
#### P1352 没有上司的舞会
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P1352)
|
||
|
||
某个大学有 $n$ 个职员,编号为 $1 \dots n$,他们之间有从属关系,也就是说他们的关系构成一棵树,父节点则为他们的直接上司。
|
||
|
||
现在有一个舞会,每个职员去了之后会增加 $r_i$ 的快乐指数,但是若一个职员的直接上司去了,那这个职员就不肯去了。
|
||
|
||
求最大快乐指数。
|
||
|
||
$1 \le n \le 6000, -128 \le r_i \le 127$
|
||
|
||
##### P1352题解
|
||
|
||
记录 $f_{u, 0/1}$ 表示以 $u$ 为根的子树,且 $u$ 选或不选的最大快乐指数, $V_u$ 为节点 $u$ 的孩子节点集合。转移方程为:
|
||
|
||
$$
|
||
\begin{aligned}
|
||
f_{u, 0} &= \sum_{v \in V_u} max(f_{v, 1}, f_{v, 0}) \\
|
||
f_{u, 1} &= r_u + \sum_{v \in V_u} f_{v, 0} \\
|
||
\end{aligned}
|
||
$$
|
||
|
||
时间复杂度 $O(n)$
|
||
|
||
##### P1352代码
|
||
|
||
```cpp
|
||
#include <stdio.h>
|
||
#include <vector>
|
||
using namespace std;
|
||
int n, fa[6005], f[6005][2], r[6005];
|
||
vector<int> child[6005];
|
||
void dp(int m)
|
||
{
|
||
for (int i = 0; i < child[m].size(); ++i)
|
||
{
|
||
dp(child[m][i]);
|
||
}
|
||
f[m][1] = r[m];
|
||
for (int i = 0; i < child[m].size(); ++i)
|
||
{
|
||
f[m][0] += max(f[child[m][i]][0], f[child[m][i]][1]);
|
||
f[m][1] += f[child[m][i]][0];
|
||
}
|
||
return;
|
||
}
|
||
int main()
|
||
{
|
||
scanf("%d", &n);
|
||
for (int i = 1; i <= n; ++i)
|
||
{
|
||
scanf("%d", &r[i]);
|
||
}
|
||
for (int i = 1; i < n; ++i)
|
||
{
|
||
int l, k;
|
||
scanf("%d%d", &l, &k);
|
||
fa[l] = k;
|
||
child[k].push_back(l);
|
||
}
|
||
int root = 1;
|
||
while (fa[root] != 0)
|
||
{
|
||
root = fa[root];
|
||
}
|
||
dp(root);
|
||
printf("%d\n", max(f[root][0], f[root][1]));
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P2899 Cell Phone Network G
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P2899)
|
||
|
||
有一个有 $n$ 个节点的树形地图,一个节点如果建立电话干,那么和他直接相连的顶点也会被覆盖到。问最少建多少个电话杆,可以使所有顶点被覆盖。
|
||
|
||
$1 \le n \le 10^4$
|
||
|
||
##### P2899题解
|
||
|
||
该题目的重点在于,一个点的选取不仅会影响孩子节点,还会影响父亲节点,所以转移会复杂一些。
|
||
|
||
- 记 $f_{i, 0}$ 表示选 $i$,且以 $i$ 为根的子树每个点都被覆盖的最少信号塔部署数量。
|
||
- 记 $f_{i, 1}$ 表示不选 $i$,且 $i$ 被儿子覆盖的最少信号塔数量。
|
||
- 记 $f_{i, 2}$ 表示不选 $i$,且以 $i$ 为根的子树每个点都被覆盖的最少信号塔部署数量,即 $i$ 要被父节点覆盖。
|
||
|
||
记 $V_i$ 为 $i$ 的子节点点集。
|
||
|
||
转移方程:
|
||
|
||
$$
|
||
f_{i, 0} = 1 + \sum_{v \in V_i} min(f_{v, 0}, f_{v, 1}, f_{v, 2}) \\
|
||
f_{i, 2} = \sum_{v \in V_i} min(f_{v, 0}, f_{v, 1}) \\
|
||
$$
|
||
|
||
如果存在 $k \in V_i \And f_{k, 0} \le f_{k, 1}$,则
|
||
|
||
$$f_{i, 1}= \sum_{v \in V_i} min(f_{v, 1}, f_{v, 0})$$
|
||
|
||
否则
|
||
|
||
$$
|
||
t \leftarrow \{min(f_{v, 0} - f_{v, 1}), v \in V_i\}\\
|
||
f_{i, 1}= \sum_{v \in V_i} min(f_{v, 1}, f_{v, 0}) + t
|
||
$$
|
||
|
||
##### P2899代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
const int N = 100005, INF = 0x7f7f7f7f;
|
||
int n, m, dp[N][5];
|
||
int nxt[N], to[N];
|
||
int head[N], cnt;
|
||
void add(int too, int from)
|
||
{
|
||
to[++cnt] = too;
|
||
nxt[cnt] = head[from];
|
||
head[from] = cnt;
|
||
}
|
||
void dfs(int u, int fa)
|
||
{
|
||
int tot = 0, g[N];
|
||
dp[u][0] = 1;
|
||
for (int i = head[u]; i; i = nxt[i])
|
||
{
|
||
int v = to[i];
|
||
if (v == fa)
|
||
continue;
|
||
dfs(v, u);
|
||
dp[u][0] += min(dp[v][0], min(dp[v][1], dp[v][2]));
|
||
dp[u][1] += dp[v][0];
|
||
dp[u][2] += min(dp[v][0], dp[v][1]);
|
||
g[++tot] = dp[v][1] - dp[v][0];
|
||
}
|
||
if (!tot)
|
||
dp[u][1] = INF;
|
||
else
|
||
{
|
||
sort(g + 1, g + 1 + tot);
|
||
for (int i = 1; i < tot; i++)
|
||
{
|
||
if (g[i] < 0)
|
||
dp[u][1] += g[i];
|
||
else
|
||
break;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
int main()
|
||
{
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("5.in", "r", stdin);
|
||
#endif
|
||
scanf("%d", &n);
|
||
for (int i = 1; i < n; i++)
|
||
{
|
||
int x, y;
|
||
scanf("%d%d", &x, &y);
|
||
add(x, y), add(y, x);
|
||
}
|
||
dfs(1, 0);
|
||
printf("%d", min(dp[1][0], dp[1][1]));
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 换根
|
||
|
||
换根 dp,又称作二次扫描,相比与普通的树形 dp,换根 dp 的特点是不具有固定的根节点。而最终要求的是哪个节点为根时答案最优。
|
||
|
||
换根 dp 解决的问题,便是在节点数为 $n$ 的树中,朴素做法则为进行 $n$ 次树形 dp,而换根 dp 则是求出换根所带来的影响,以降低 $n$ 倍的复杂度。
|
||
|
||
换根后,大部分的情况不变,变得只是旧根和新根的子节点的父子关系,所以只需要处理这些关系后,即可单次树形 dp 求出所有点为根的答案。
|
||
|
||
主要步骤:
|
||
|
||
1. 定根求解
|
||
2. 计算换根
|
||
|
||
### 换根 dp 例题
|
||
|
||
#### P3478 STA-Station
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P3478)
|
||
|
||
给定一个 $n$ 个点的树,求出一个节点,使得这个节点为根时,所有节点的深度之和最大。
|
||
|
||
$1 \le n \le 10^6$
|
||
|
||
##### P3478题解
|
||
|
||
若直接进行 $n$ 次树形 dp,时间肯定会爆。
|
||
|
||
考虑固定 $1$ 号节点为根,则可以直接求出以 $u$ 为根的子树内所有节点的深度和 $f_u$。
|
||
|
||
接下来考虑换根,使用 $f_u$ 表示以 $u$ 为根节点时所有节点的深度和。
|
||
|
||
记 $u$ 为 $v$ 的父亲, $sz_u$ 表示以 $u$ 为根的子树大小(以 $1$ 为根时),则转移方程为
|
||
|
||
$$
|
||
f_v = f_u - sz_v + (sz_1 - sz_v)
|
||
$$
|
||
|
||
这个转移方程的含义是:
|
||
|
||
- 第一个 $-sz_v$ 表示根节点为 $v$ 时,原来根节点为 $v$ 的子树的所有深度都 $-1$
|
||
- 第二个 $+(sz_1 - sz_v)$ 表示根节点为 $v$ 时,原来根节点为 $u$ 且不在 $v$ 子树内的节点深度都 $+1$
|
||
|
||
这样就可以快速求出换根后对答案产生的贡献。
|
||
|
||
##### P3478代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
const int N = 3e6 + 10;
|
||
int n;
|
||
int to[N], nxt[N];
|
||
int dep[N], head[N], tot;
|
||
void add(int a, int b)
|
||
{
|
||
to[++tot] = b, nxt[tot] = head[a], head[a] = tot;
|
||
}
|
||
int siz[N];
|
||
long long dp[N];
|
||
void search(int u, int fa)
|
||
{
|
||
siz[u] = 1, dp[u] = dep[u];
|
||
for (int i = head[u]; i; i = nxt[i])
|
||
{
|
||
int v = to[i];
|
||
if (v == fa)
|
||
continue;
|
||
dep[v] = dep[u] + 1;
|
||
search(v, u);
|
||
siz[u] += siz[v], dp[u] += dp[v];
|
||
}
|
||
}
|
||
void chroot(int u, int fa)
|
||
{
|
||
for (int i = head[u]; i; i = nxt[i])
|
||
{
|
||
int v = to[i];
|
||
if (v == fa)
|
||
continue;
|
||
dp[v] = dp[u] - siz[v] + n - siz[v];
|
||
chroot(v, u);
|
||
}
|
||
}
|
||
int main()
|
||
{
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("3.in", "r", stdin);
|
||
#endif
|
||
scanf("%d", &n);
|
||
for (int i = 1; i < n; i++)
|
||
{
|
||
int a, b;
|
||
scanf("%d%d", &a, &b);
|
||
add(a, b);
|
||
add(b, a);
|
||
}
|
||
search(1, 0);
|
||
chroot(1, 0);
|
||
int ans = 0;
|
||
for (int i = 1; i <= n; i++)
|
||
ans = (dp[ans] < dp[i]) ? i : ans;
|
||
printf("%d\n", ans);
|
||
}
|
||
```
|
||
|
||
#### CF543D Road Improvement
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/CF543D)
|
||
|
||
一个国家有 $n$ 座城市和 $n - 1$ 条双向道路,你可以沿道路从一个城市到任意一个其他城市。这些城市被编号 $1 \dots n$
|
||
|
||
所有最初的城市都是不良的,但是政府想要改善道路情况。我们认为从首都 $x$ 到其他城市的道路上的不良道路数量 $\le 1$,则市民会感到满意。求对于每个可能的 $x$,求出所有能满足市民条件的改善道路方案的数量 $\bmod 10^9 + 7$
|
||
|
||
$2 \le n \le 2 \times 10^5$
|
||
|
||
##### CF534D题解
|
||
|
||
考虑定根的情况,记 $f_i$ 表示以 $i$ 为根的子树改良方案数量,那么对于所有叶子节点, $f_i = 1$
|
||
|
||
对于非叶子节点 $u$,记 $v \in V_u$,若 $u, v$ 之间没有改良,那么 $v$ 子树内所有道路都要改良,方案数为 $1$。如果这条道路改良了,那么方案数为 $f_v$。转移为
|
||
|
||
$$f_u = \prod_{v \in V_u} (f_v + 1)$$
|
||
|
||
接下来考虑换根。
|
||
|
||
发现在将根从 $u$ 变为他的儿子 $v$ 的时候,只有 $f_u, f_v$ 发生变化。
|
||
|
||
设 $f'_u$ 为换根后 $f_u$ 的新值。则
|
||
|
||
$$
|
||
f'_u = \prod_{k \in V_u, k \not= v}(f_k + 1) \\
|
||
f'_v = f_v(f_u + 1) \\
|
||
$$
|
||
|
||
即可换根求解。
|
||
|
||
##### CF534D代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
const int mod = 1e9 + 7, M = 5e5 + 10;
|
||
using namespace std;
|
||
int n, nxt[M], head[M], to[M], tot;
|
||
long long F[M], ans[M];
|
||
vector<long long> F1[M], F2[M];
|
||
int read()
|
||
{
|
||
char c = getchar();
|
||
int ans = 0;
|
||
while (c < '0' || c > '9')
|
||
c = getchar();
|
||
while (c >= '0' && c <= '9')
|
||
ans = (ans << 1) + (ans << 3) + (c ^ 48), c = getchar();
|
||
return ans;
|
||
}
|
||
void add(int u, int v)
|
||
{
|
||
nxt[++tot] = head[u], head[u] = tot, to[tot] = v, nxt[++tot] = head[v], head[v] = tot, to[tot] = u;
|
||
}
|
||
void dfs1(int x, int fa)
|
||
{
|
||
F[x] = 1;
|
||
for (int i = head[x]; i; i = nxt[i])
|
||
if (to[i] != fa)
|
||
dfs1(to[i], x), F[x] = F[x] * (F[to[i]] + 1) % mod;
|
||
}
|
||
void dfs2(int x, int fa)
|
||
{
|
||
ans[x] = 1;
|
||
for (int i = head[x]; i; i = nxt[i])
|
||
{
|
||
ans[x] = ans[x] * (F[to[i]] + 1) % mod;
|
||
if (to[i] != fa)
|
||
F1[x].push_back(F[to[i]] + 1), F2[x].push_back(F[to[i]] + 1);
|
||
}
|
||
for (int i = 1; i < F1[x].size(); i++)
|
||
F1[x][i] = F1[x][i] * F1[x][i - 1] % mod;
|
||
for (int i = F2[x].size() - 2; i >= 0; i--)
|
||
F2[x][i] = F2[x][i] * F2[x][i + 1] % mod;
|
||
int cnt = 0;
|
||
for (int i = head[x]; i; i = nxt[i])
|
||
{
|
||
if (to[i] == fa)
|
||
continue;
|
||
F[x] = (fa ? F[fa] + 1 : 1);
|
||
if (cnt > 0)
|
||
F[x] = F[x] * F1[x][cnt - 1] % mod;
|
||
if (cnt < F2[x].size() - 1)
|
||
F[x] = F[x] * F2[x][cnt + 1] % mod;
|
||
dfs2(to[i], x);
|
||
++cnt;
|
||
}
|
||
}
|
||
int main()
|
||
{
|
||
n = read();
|
||
for (int i = 2; i <= n; i++)
|
||
add(read(), i);
|
||
dfs1(1, 0);
|
||
dfs2(1, 0);
|
||
for (int i = 1; i <= n; i++)
|
||
printf("%lld ", ans[i]);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 状压
|
||
|
||
状态压缩 dp,表示将一个集合压缩到一个二进制数来表示状态,常用于关于图中选择节点等的问题,这种问题的节点个数通常很少,一般 $\le 20$。并且,这种问题的转移通常与选择的节点顺序无关,只和最后一个选择的节点有关。
|
||
|
||
我们常用 $f_{S, u}$ 表示经过了 $S$ 中的节点,且最后一个经过的节点为 $u$ 的结果。
|
||
|
||
当然,状压 dp 也可以用在线性结构上,一维 $i$ 表示扫描进度,一维 $S$ 进行状态压缩。[(例题)](#p6239-奇怪的道路)
|
||
|
||
### 引入例题 旅行商问题
|
||
|
||
给定一个 $n$ 个顶点组成的带权有向图的距离矩阵 $d(i, j)$。要求从顶点 $0$ 出发,经过每个节点恰好一次后再回到节点 $0$
|
||
|
||
问经过的边权之和最短是多少。
|
||
|
||
$n \le 15$
|
||
|
||
#### 旅行商问题题解
|
||
|
||
所有的路线有 $(n - 1)!$ 种。
|
||
|
||
因为已经经过的点,除了最后一个,其他点的顺序对于后续没有影响,所以考虑使用 $f_{S, v}$ 表示已经经过了 $S$ 中的点,上一个经过的点是 $v$ 的情况下经过的边权之和最短的值。
|
||
|
||
但是因为数组下标不能为数组,所以用一个 $2^{15}$ 的 int 来表示状态,这便是状态压缩。即
|
||
|
||
$$
|
||
mask(S) = \sum_{i \in S} 2^i
|
||
$$
|
||
|
||
转移:
|
||
|
||
$$
|
||
f_{\varnothing, 0} = 0 \\
|
||
f_{S, v} + d(u, v) \rightarrow f_{S \cup u, u}, u \notin S
|
||
$$
|
||
|
||
### 常见技巧
|
||
|
||
#### 位运算操作
|
||
|
||
```cpp
|
||
1 << i // 只包含 i 的集合
|
||
(1 << n) - 1 // 1...n - 1 全集
|
||
if (S >> i & 1) // 判断 i in S
|
||
S | (1 << i) // S and i
|
||
S & ~(1 << i) // S without i
|
||
S | T // 并
|
||
S & T // 交
|
||
```
|
||
|
||
注意优先级:左右移 > 按位或,按位与 > 按位取反
|
||
|
||
#### popcount
|
||
|
||
popcount 定义:一个数的二进制中 $1$ 的个数。
|
||
|
||
常见求法:
|
||
|
||
- 内置函数 `__builtin_popcount(), __builtin_popcountll()`
|
||
- 预处理
|
||
|
||
```cpp
|
||
int n;
|
||
int c[1 << 20] = {0};
|
||
int main()
|
||
{
|
||
for (int S = 1; S <= (1 << n); ++S)
|
||
c[S] = c[S ^ (S & (-S))] + 1;
|
||
}
|
||
int query(long long x)
|
||
{
|
||
return c[x & ((1 << 20) - 1)] + c[x >> 20];
|
||
}
|
||
```
|
||
|
||
### 状压 dp 例题
|
||
|
||
#### P2831 愤怒的小鸟
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P2831)
|
||
|
||
平面上有若干只猪 $(x_i, y_i)$,小鸟的飞行轨迹为 $ax^2 + bx$。当一只猪在小鸟的飞行轨迹上,这只猪将会被消灭,且不会影响小鸟的飞行。求最受需要多少只鸟才能把所有猪消灭。
|
||
|
||
##### P2831题解
|
||
|
||
考虑用 $f_S$ 表示覆盖 $S$ 这个集合所需要的最小数量, $g_{i, j}$ 表示经过 $i, j$ 两个点的抛物线所经过的集合。
|
||
|
||
$f_{S|g_{i, j}} = min(f_{S|g_{i, j}}, f_S + 1)$
|
||
|
||
##### P2831代码
|
||
|
||
```cpp
|
||
```
|
||
|
||
#### P1896 互不侵犯
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P1896)
|
||
|
||
在 $N \times N$ 的棋盘里面放 $K$ 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 $8$ 个格子。
|
||
|
||
$1 \le N \le 9$,$0 \le K \le N \times N$
|
||
|
||
##### P1896题解
|
||
|
||
使用 $f_{i, j, l}$ 表示只考虑前 $i$ 行,第 $i$ 行按照编号 $j$ 的状态放置国王,且已经放置 $l$ 个国王。
|
||
|
||
因此,我们用二进制数 $sit_j$ 表示国王的放置情况,使用 $sta_j$ 表示该状态下的国王个数。因此,只需要在程序开始时枚举出行内的每种情况,转移的时候保证和上一行的情况不冲突即可。转移方程:
|
||
|
||
$$
|
||
f_{i, j, l} = \sum f_{i - 1, x, l - sta(j)} \\
|
||
(sit_x << 1) \And sit(j)==0 \\
|
||
(sit_x >> 1) \And sit(j)==0 \\
|
||
$$
|
||
|
||
##### P1896代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
#define int long long
|
||
using namespace std;
|
||
int N, K, cnt, a[1024], b[1024], f[20][1024][90];
|
||
void dfs(int k, int mask, int count)
|
||
{
|
||
if (k > N)
|
||
{
|
||
++cnt;
|
||
a[cnt] = mask;
|
||
b[cnt] = count;
|
||
return;
|
||
}
|
||
dfs(k + 1, mask * 2, count);
|
||
if ((mask & 1) == 0 && count < K)
|
||
dfs(k + 1, mask * 2 + 1, count + 1);
|
||
}
|
||
signed main()
|
||
{
|
||
scanf("%lld%lld", &N, &K);
|
||
dfs(1, 0, 0);
|
||
f[0][1][0] = 1;
|
||
for (int i = 1; i <= N; ++i)
|
||
{
|
||
for (int j = 1; j <= cnt; ++j)
|
||
{
|
||
for (int k = b[j]; k <= K; ++k)
|
||
{
|
||
for (int last = 1; last <= cnt; ++last)
|
||
{
|
||
if (a[last] & a[j])
|
||
continue;
|
||
if ((a[last] << 1) & a[j])
|
||
continue;
|
||
if ((a[last] >> 1) & a[j])
|
||
continue;
|
||
f[i][j][k] += f[i - 1][last][k - b[j]];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
int ans = f[N][1][K];
|
||
for (int j = 2; j <= cnt; ++j)
|
||
{
|
||
ans += f[N][j][K];
|
||
}
|
||
printf("%lld\n", ans);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P4460 解锁屏幕
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P4460)
|
||
|
||
求平面上给定坐标的 $n$ 个点中选出一条点数不小于 $4$,且经过的路径上的点必须被选中,可以重复经过已经经过的点但是每个点只能被选中一次的路径数量。
|
||
|
||

|
||
|
||
$1 \le n \le 20$
|
||
|
||
##### P4460题解
|
||
|
||
考虑 $f_{S, i}$ 表示经过了 $S$,最后一个节点为 $i$ 的方案数。
|
||
|
||
转移时选择一个 $j$ 满足 $j \in S \And \forall k \in \{S - j\}, P_k \notin P_iP_j$
|
||
|
||
其中 $P_k \notin P_iP_j$ 表示 $k$ 不在 $i, j$ 的路径中。
|
||
|
||
这样的时间复杂度为 $O(2_nn^3)$,无法通过,需要至少降掉一个 $n$ 才可以通过。
|
||
|
||
于是考虑预处理 $g_{i, j}$ 表示 $P_i, P_j$ 经过的所有点的集合,枚举每一个点进行转移。
|
||
|
||
这样,复杂度就降为了 $O(n^22^n)$,可以通过。
|
||
|
||
##### P4460代码
|
||
|
||
```cpp
|
||
#include <bits/stdc++.h>
|
||
using namespace std;
|
||
const int MOD = 100000007;
|
||
pair<int, int> p[25];
|
||
int dp[2500005][25], ans;
|
||
int f[25][25]; // 过i,j必须经过的点
|
||
bool cmp(pair<int, int> a, pair<int, int> b)
|
||
{
|
||
return a.first < b.first || (a.first == b.first && a.second < b.second);
|
||
}
|
||
long double work(int x1, int y1, int x2, int y2)
|
||
{
|
||
if (x1 == x2)
|
||
return INT_MAX;
|
||
return 1.0 * (y1 - y2) / (x1 - x2);
|
||
}
|
||
int n;
|
||
int main()
|
||
{
|
||
#ifndef ONLINE_JUDGE
|
||
freopen("1.in", "r", stdin);
|
||
#endif
|
||
scanf("%d", &n);
|
||
for (int i = 1; i <= n; i++)
|
||
scanf("%d%d", &p[i].first, &p[i].second);
|
||
// pre begin
|
||
sort(p + 1, p + n + 1, cmp);
|
||
for (int i = 1; i <= n; i++)
|
||
for (int j = i + 1; j <= n; j++)
|
||
for (int k = i + 1; k <= j - 1; k++)
|
||
if (abs(work(p[i].first, p[i].second, p[k].first, p[k].second) - work(p[k].first, p[k].second, p[j].first, p[j].second)) <= 1e-7)
|
||
f[i][j] |= (1 << k), f[j][i] |= (1 << k);
|
||
// pre end
|
||
for (int i = 1; i <= n; i++)
|
||
dp[1 << i][i] = 1;
|
||
for (int i = 0; i < (1 << (n + 1)); i++)
|
||
for (int j = 1; j <= n; j++)
|
||
if (i & (1 << j))
|
||
for (int k = 1; k <= n; k++)
|
||
if ((i & (1 << k)) == 0 && k != j && (i & f[j][k]) == f[j][k])
|
||
dp[i | (1 << k)][k] = (dp[i | (1 << k)][k] + dp[i][j]) % MOD;
|
||
for (int i = 0; i < (1 << (n + 1)); i++)
|
||
if (__builtin_popcount(i) >= 4)
|
||
for (int j = 1; j <= n; j++)
|
||
if (i & (1 << j))
|
||
ans = (ans + dp[i][j]) % MOD;
|
||
printf("%d\n", ans);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
#### P6239 奇怪的道路
|
||
|
||
[题目链接](https://www.luogu.com.cn/problem/P6239)
|
||
|
||
求有 $n$ 个点, $m$ 条边,每个点的度数都是偶数,且每条边连接的节点编号相差不超过 $k$ 的无向图的数量 $\bmod 10^9 + 7$。
|
||
|
||
$1 \le n, m \le 30, 1 \le k \le 8$
|
||
|
||
##### P6239题解
|
||
|
||
使用 $f(i, j, S)$ 表示考虑了前 $i$ 个点,一共连接了 $j$ 条边, $[i - k, i - 1]$ 内的点目前度数奇偶性为 $S$ 的方案数。考虑每次转移加入一个点。
|
||
|
||
##### P6239代码
|
||
|
||
---
|