DP 基础
基础
本文记录了一些基础的
状压
状压 std::bitset
)来实现优化复杂度的一个
状压
一些位运算常见的技巧:
- 对于集合中的元素,最好在一开始读入的时候就以
读入,这样便于压缩进二进制集合中。
例题
[SCOI2005] 互不侵犯
题意:在
的国际象棋的棋盘里面放 个国王,使他们互不攻击,共有多少种摆放方案。
定义
考虑什么时候可以转移,定义当前行的状态为
- 如果
,则说明 与 左中右三格之内没有交集,即这一行的每一个国王都不在上一行国王能吃的范围内,意味可以进行转移:
- 否则则说明这一行的国王会与上一行的国王冲突,转移不能进行。
讲一讲这题的特殊的
注意到有许多状态在行这个层面本身就是不合法的,例如
因此,我们可以先做一个预处理,筛选出所有合法的状态,在每一轮转移时,直接枚举合法的状态即可。
更进一步,我们其实还可以预处理出对于任意一个状态
时间复杂度不太好分析,其上界为
事实上,本题也可以使用滚动数组压缩一维空间,也可以使用矩阵快速幂优化,但本题数据范围较小,故不用。
代码:
#include <bits/extc++.h>
#define inline __always_inline
#define popcnt(x) __builtin_popcount(x)
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 10, MaxA = 1 << 9, MaxM = 85;
int n, m, mask, cnt = 0, s[MaxA];
std::vector<int> trans[MaxA];
int64_t f[MaxN][MaxA][MaxM];
int main()
{
read(n), read(m);
mask = (1 << n) - 1;
for (int x = 0; x <= mask; x++)
if (!(x & (x << 1 | x >> 1)))
f[1][cnt][popcnt(x)] = 1, s[cnt++] = x;
for (int j = 0; j < cnt; j++)
for (int k = 0; k < cnt; k++)
if (!(s[k] & (s[j] << 1 | s[j] | s[j] >> 1)))
trans[j].push_back(k);
for (int i = 2; i <= n; i++)
for (int j = 0; j < cnt; j++)
for (auto &&k : trans[j])
for (int t = popcnt(s[k]); t <= m; t++)
f[i][k][t] += f[i - 1][j][t - popcnt(s[k])];
int64_t ans = 0;
for (int x = 0; x < cnt; x++) ans += f[n][x][m];
printf("%ld", ans);
return 0;
}
多倍经验(类似题,不能套代码,需要改动代码逻辑):
- [蓝桥杯 2021 省 AB2] 国际象棋:难度相近的题,稍稍更改判定转移是否合法的逻辑即可。
- [USACO06NOV] Corn Fields G:[NOI2001] 炮兵阵地 的弱化版
- *[NOI2001] 炮兵阵地:略有难度的加强版,不过稍加思考仍容易做出。注意多优化,如果空间被卡可以考虑滚动数组。
[POI2004] PRZ
定义
其中
如果预处理不影响代码的时间复杂度,可以预处理出尽可能多的常用信息以减小常数,简化编码。
预处理
#define MSB(x) std::__lg(x)
for (int i = 1; i <= mask; i++)
{
T[i] = std::max(T[i ^ 1 << MSB(i)], t[MSB(i)]);
W[i] = W[i ^ 1 << MSB(i)] + w[MSB(i)];
}
其中
本题还用到了一个
对于一个二进制集合
for (int T = S; ; T = T - 1 & S)
{
// do something here.
if (!T) break;
}
上述算法等效于忽略了
总代码:
#include <bits/extc++.h>
#define inline __always_inline
#define MSB(x) std::__lg(x)
template <typename T> inline void chkmin(T &x, const T &y) { if (x > y) x = y; }
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 16, MaxS = 1 << MaxN;
int m, n, mask, t[MaxN], w[MaxN], T[MaxS], W[MaxS], f[MaxS];
int main()
{
read(m), read(n), mask = (1 << n) - 1;
for (int i = 0; i < n; i++) read(t[i]), read(w[i]);
for (int i = 1; i <= mask; i++)
{
T[i] = std::max(T[i ^ 1 << MSB(i)], t[MSB(i)]);
W[i] = W[i ^ 1 << MSB(i)] + w[MSB(i)];
}
memset(f, 0x3f, sizeof(f)), f[0] = 0;
for (int i = 1; i <= mask; i++)
for (int j = i - 1 & i; ; j = j - 1 & i)
{
if (W[i ^ j] <= m) chkmin(f[i], f[j] + T[i ^ j]);
if (!j) break;
}
printf("%d", f[mask]);
return 0;
}
*花园
小 L 有一座环形花园,沿花园的顺时针方向,他把各个花圃编号为
。花园 和 是相邻的。
任意相邻个花圃中都只有不超过 个 C 形的花圃,其余花圃均为 P 形的花圃。
请帮小 L 求出符合规则的花园种数对取模的结果。
, , 。
本题的
定义
稍加思考可以推出转移:
考虑如何统计答案,这里需要注意一个 误区,直接统计
正确的做法是枚举每一种初始状态
同时还可以利用滚动数组压缩一维,就可以取得
#include <bits/extc++.h>
#define inline __always_inline
#define popcnt(x) __builtin_popcount(x)
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 1e5 + 5, MaxM = 5, MaxA = 1 << MaxM, mod = 1e9 + 7;
int64_t n;
int m, k, mask, s[MaxA], cnt = 0, f[2][MaxA];
int main()
{
read(n), read(m), read(k), mask = (1 << m) - 1;
for (int i = 0; i <= mask; i++)
if (popcnt(i) <= k) s[cnt++] = i;
int ans = 0;
for (int t = 0; t < cnt; t++)
{
memset(f[0], 0, sizeof(f[0])), f[0][s[t]] = 1;
for (int i = 1; i <= n; i++)
{
int prev = i - 1 & 1, next = i & 1;
memset(f[next], 0, sizeof(f[next]));
for (int j = 0; j < cnt; j++)
{
int x = s[j] >> 1 | 1 << m - 1;
(f[next][s[j]] += f[prev][s[j] >> 1]) %= mod;
if (popcnt(x) <= k)
(f[next][s[j]] += f[prev][x]) %= mod;
}
}
(ans += f[n & 1][s[t]]) %= mod;
}
printf("%d", ans);
return 0;
}
考虑进一步优化,由于
矩阵快速幂只需要使用普通的
注意任何矩阵快速幂,如果乘法过程中有可能爆 long long
,因为在数次乘法之后,矩阵中的值可能非常大。
#include <bits/extc++.h>
#define inline __always_inline
#define popcnt(x) __builtin_popcount(x)
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 1e5 + 5, MaxM = 5, MaxA = 1 << MaxM, mod = 1e9 + 7;
int64_t n;
int m, k, mask, s[MaxA], cnt = 0, f[2][MaxA];
struct vector_t
{
int v[MaxA];
inline void clear() { memset(v, 0, sizeof(v)); }
inline auto &operator[](int x) { return v[x]; }
inline const auto &operator[](int x) const { return v[x]; }
} v;
struct matrix_t
{
int M[MaxA][MaxA];
inline void unit() { for (int i = 0; i < MaxA; i++) M[i][i] = 1; }
inline void clear() { memset(M, 0, sizeof(M)); }
inline auto &operator[](int x) { return M[x]; }
inline const auto &operator[](int x) const { return M[x]; }
} trans;
inline matrix_t operator*(const matrix_t &x, const matrix_t &y)
{
matrix_t m; m.clear();
for (int i = 0; i < MaxA; i++)
for (int k = 0; k < MaxA; k++)
for (int j = 0; j < MaxA; j++)
m[i][j] = (m[i][j] + 1l * x[i][k] * y[k][j]) % mod; // 不开 long long 见祖宗!!!
return m;
}
inline vector_t operator*(const matrix_t &x, const vector_t &y)
{
vector_t v; v.clear();
for (int i = 0; i < MaxA; i++)
for (int j = 0; j < MaxA; j++)
v[i] = (v[i] + 1l * x[i][j] * y[j]) % mod;
return v;
}
inline matrix_t operator^(matrix_t x, int64_t n)
{
matrix_t m; m.clear(), m.unit();
for (; n; n >>= 1, x = x * x)
if (n & 1) m = m * x;
return m;
}
int main()
{
read(n), read(m), read(k), mask = (1 << m) - 1;
for (int i = 0; i <= mask; i++)
if (popcnt(i) <= k)
{
s[cnt++] = i;
trans[i][i >> 1] = 1;
int j = i >> 1 | 1 << m - 1;
if (popcnt(j) <= k)
trans[i][j] = 1;
}
trans = trans ^ n;
int ans = 0;
for (int t = 0; t < cnt; t++)
{
v[s[t]] = 1;
(ans += (trans * v)[s[t]]) %= mod;
v[s[t]] = 0;
}
printf("%d", ans);
return 0;
}
*[USACO13NOV] No Change G
你有
个硬币,面值 ,希望购买 件商品,价格 。
你必须按顺序购买,记上一次购买完了前件商品,这一次你可以花恰好一枚硬币来购买 件商品(没有找零)
请计算购买完件商品你最多能剩下多少钱。
。
定义
其中
本题
#define LSB(x) (__builtin_ctz(x))
for (int T = S; T; T ^= T & -T)
{
int x = LSB(T);
// do something here.
}
其中
代码:
#include <bits/extc++.h>
#define inline __always_inline
#define MSB(x) (31 - __builtin_clz(x))
#define LSB(x) (__builtin_ctz(x))
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxK = 16, MaxA = 1 << MaxK, MaxN = 1e5 + 5;
int k, n, mask, w[MaxK], c[MaxN], f[MaxN];
int64_t sum = 0, g[MaxN];
inline int diff(int l, int r) { return c[r] - c[l - 1]; }
inline int find(int s, int x)
{
int l = s, r = n + 1;
while (r - l > 1)
{
int mid = l + r >> 1, c = diff(s, mid);
if (x < c) r = mid;
else l = mid;
}
return l;
}
int main()
{
read(k), read(n), mask = (1 << k) - 1;
for (int i = 0; i < k; i++) read(w[i]), sum += w[i];
for (int i = 1; i <= n; i++) read(c[i]), c[i] += c[i - 1];
int64_t ans = -1;
for (int i = 1; i <= mask; i++)
{
g[i] = g[i ^ 1 << MSB(i)] + w[MSB(i)];
for (int j = i; j; j ^= j & -j)
{
int x = LSB(j);
if (f[i ^ 1 << x] == n) { f[i] = n; break; }
chkmax(f[i], find(f[i ^ 1 << x] + 1, w[x]));
}
if (f[i] == n) chkmax(ans, sum - g[i]);
}
printf("%ld", ans);
return 0;
}
树形
树形 DP,顾名思义,就是把
树上背包
换根
换根
- 先计算
遍历树,对于一个节点 与其孩子 ,根据题目的要求,由 转移出 。
Tree Painting
*[APIO2014] 连珠线
本题很有难度,是一道练思维的好题。
首先先观察题目。对于一次 Insert
操作,可以发现该操作会产生由两条蓝线连接的三个点,其形态为:
因此可以考虑把中间的点
但稍加思考就会发现这样很难推导转移,因为不好枚举与
因此可以考虑更改状态设计,强制规定
但这样答案枚举不完全,因此需要考虑以每个节点为根节点独立地做一遍
稍加思考可以推出转移:
考虑使用换根
则原
如何维护
一个推转移的
- 尽可能多地预处理出值,使得你的每一条
公式看起来尽可能简洁,尽管变量的个数可能变多,但单个式子更加简单,更容易观察出性质。
代码:
#include <bits/extc++.h>
#define inline __always_inline
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void chkmax(T &x, T &y, const T &z)
{
if (x <= z) y = x, x = z;
else if (y < z) y = z;
}
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 2e5 + 5, inf = 2e9;
int n, f[MaxN][2], d[MaxN][3], F[MaxN], g[MaxN][2];
struct edge_t { int v, w; };
std::vector<edge_t> graph[MaxN];
inline void add_edge(int u, int v, int w) { graph[u].push_back({v, w}), graph[v].push_back({u, w}); }
void dfs(int u, int p)
{
d[u][1] = d[u][2] = -inf; f[u][0] = 0;
for (auto &&[v, w] : graph[u])
if (v != p)
{
dfs(v, u);
F[v] = std::max(f[v][0], f[v][1] + w);
d[v][0] = f[v][0] + w - F[v];
chkmax(d[u][1], d[u][2], d[v][0]);
f[u][0] += F[v];
}
f[u][1] = f[u][0] + d[u][1];
}
void chroot(int u, int p)
{
for (auto &&[v, w] : graph[u])
if (v != p)
{
int f_u[2] = {f[u][0] - F[v], f_u[0] + (d[v][0] != d[u][1] ? d[u][1] : d[u][2])}; // special trick!
int F_u = std::max(f_u[0], f_u[1] + w);
f[v][0] += F_u;
chkmax(d[v][1], d[v][2], f_u[0] + w - F_u);
f[v][1] = f[v][0] + d[v][1];
chroot(v, u);
}
}
int main()
{
read(n);
for (int i = 1, u, v, w; i < n; i++) read(u), read(v), read(w), add_edge(u, v, w);
dfs(1, 0);
chroot(1, 0);
int ans = 0;
for (int i = 1; i <= n; i++) chkmax(ans, f[i][0]);
printf("%d", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?