POJ 动态规划 20 题完整题解 + 多解法对比 + 核心代码骨架。
如果你其实更想要 HDU 数据结构专题 或 Codeforces 构造/DP 专题,回复对应关键词即可换。
说明:代码为 C++17 核心骨架,省略主函数和部分输入输出;重点看状态、转移和多解法差异。
目录
编号 题目 类型
1 POJ 1014 Dividing 多重背包可行性
2 POJ 1742 Coins 多重部分和
3 POJ 1276 Cash Machine 多重背包最大可达
4 POJ 3017 Cut the Sequence 单调队列优化 DP
5 POJ 2373 Dividing the Path 单调队列优化 DP
6 POJ 1821 Fence 单调队列优化 DP
7 POJ 1159 Palindrome LCS / 区间 DP
8 POJ 3280 Cheapest Palindrome 区间 DP
9 POJ 2955 Brackets 区间 DP
10 POJ 1141 Brackets Sequence 区间 DP + 路径
11 POJ 1651 Multiplication Puzzle 区间 DP
12 POJ 1191 棋盘分割 区间 DP / 记忆化
13 POJ 1160 Post Office 区间 DP + 四边形不等式
14 POJ 3254 Corn Fields 状压 DP
15 POJ 1185 炮兵阵地 状压 DP
16 POJ 2411 Mondriaan's Dream 状压 / 轮廓线 DP
17 POJ 2282 The Counting Problem 数位 DP / 逐位统计
18 POJ 3252 Round Numbers 数位 DP / 组合
19 POJ 1088 滑雪 记忆化搜索 / 排序 DP
20 POJ 2342 Anniversary party 树形 DP
- POJ 1014 Dividing
模型:有 6 种价值为 1~6 的物品,数量给出,问能否平分总价值。
状态:fj 表示是否能凑出价值 j。
多解法对比
解法 思路 复杂度 特点
二进制拆分 + 01 背包 把数量拆成 1,2,4,... O(sum log cnt) 最好写
DFS + 剪枝 从大到小搜索 实际很快 数据强时需剪枝
单调队列多重背包 按余数分类 O(6 * sum) 最稳但难写
核心代码:二进制拆分
cpp
bool fMAXN;
vector w;
for (int v = 1; v <= 6; v++) {
int c; cin >> c;
for (int k = 1; k <= c; k <<= 1) {
w.push_back(k * v);
c -= k;
}
if © w.push_back(c * v);
}
f0 = true;
for (int x : w)
for (int j = sum; j >= x; j--)
fj |= fj - x;
if (sum % 2 == 0 && fsum / 2) cout << "Can be divided.\n";
- POJ 1742 Coins
模型:多重部分和,问 1~m 中有多少值能被凑出。
状态:fj 是否可达;usedj 当前硬币已用数量。
多解法对比
解法 复杂度 特点
used 数组 O(nm) 最经典,必须掌握
二进制 + bitset O(nm/word) 常数小
单调队列 O(nm) 理论最优,代码复杂
核心代码:used 数组
cpp
bool fMAXM;
int usedMAXM;
f0 = true;
int ans = 0;
for (int i = 1; i <= n; i++) {
int a, c; cin >> a >> c;
memset(used, 0, sizeof(int) * (m + 1));
for (int j = a; j <= m; j++) {
if (!fj && fj - a && usedj - a < c) {
fj = true;
usedj = usedj - a + 1;
ans++;
}
}
}
cout << ans << '\n';
- POJ 1276 Cash Machine
模型:多重背包,容量 cash,每种面额有数量,求最大可达金额。
多解法对比
解法 复杂度 特点
二进制拆分 + 01 背包 O(sum log cnt) 推荐
单调队列优化 O(n * cash) 更快但难写
核心代码
cpp
bool fMAXC;
f0 = true;
for (int i = 1; i <= n; i++) {
int v, c; cin >> c >> v;
for (int k = 1; k <= c; k <<= 1) {
int x = k * v;
for (int j = cash; j >= x; j--)
fj |= fj - x;
c -= k;
}
if © {
int x = c * v;
for (int j = cash; j >= x; j--)
fj |= fj - x;
}
}
for (int j = cash; j >= 0; j--)
if (fj) { cout << j << '\n'; break; }
-
POJ 3017 Cut the Sequence
模型:把序列分成若干段,每段和不超过 M,最小化每段最大值之和。
状态:
text
dpi = min(dpj + max(aj+1...i)),且 sum(j+1...i) <= M
多解法对比
解法 复杂度 特点
暴力 DP O(n^2) 只能小数据
单调队列 + multiset O(n log n) 标准解
线段树维护 O(n log n) 更直观
核心思路
-
用双指针维护 sum <= M 的最左合法 j。
-
单调队列维护 a 的递减序列。
-
队列中每个元素代表一段 j,该段内 max(aj+1...i) 固定。
-
用 multiset 维护候选 dpj + max。
伪代码
cpp
deque q;
multiset st;
int L = 1, sum = 0;
for (int i = 1; i <= n; i++) {
sum += ai;
while (sum > M) sum -= aL++;
while (!q.empty() && aq.back() <= ai) {
// 删除旧候选
q.pop_back();
}
q.push_back(i);
// 删除越界队头
while (!q.empty() && q.front() < L) q.pop_front();
// 更新候选,具体实现需维护每个队头对应的最优 dp
dpi = *st.begin();
}
- POJ 2373 Dividing the Path
模型:给区间 0, L 喷水,每个喷头覆盖长度 2A, 2B 且为偶数,不能覆盖牛的区域,求最少喷头数。
状态:
text
dpi = min(dpj + 1)
i - 2B <= j <= i - 2A
i, j 为偶数
j, i 不覆盖牛区
多解法对比
解法 复杂度 特点
暴力枚举 j O(L * B) 超时
单调队列 O(L) 标准解
核心代码
cpp
for (int i = 2; i <= L; i += 2) {
if (badi) continue;
while (!q.empty() && q.front() < i - 2 * B) q.pop_front();
if (i - 2 * A >= 0) {
int j = i - 2 * A;
if (dpj != INF) {
while (!q.empty() && dpq.back() >= dpj) q.pop_back();
q.push_back(j);
}
}
if (!q.empty()) dpi = dpq.front() + 1;
}
-
POJ 1821 Fence
模型:K 个工人刷墙,每个工人必须刷包含位置 s 的连续段,长度不超过 L,每格收益 p,求最大收益。
状态:
text
dpij = 前 i 个工人刷前 j 块的最大收益
转移
不刷:
text
dpij = dpi-1j
第 i 个工人刷 k+1...j:
text
dpij = max(dpi-1k + p_i * (j - k))
k < s_i <= j
j - k <= L_i
多解法对比
解法 复杂度 特点
暴力枚举 k O(K N^2) 超时
单调队列优化 O(K N) 标准
核心代码
cpp
for (int i = 1; i <= K; i++) {
deque q;
for (int j = 0; j <= N; j++) {
dpij = dpi-1j;
if (j) dpij = max(dpij, dpij-1);
if (j >= s[i]) { int k = j - L[i]; if (k >= 0) { while (!q.empty() && dp[i-1][q.back()] - p[i] * q.back() <= dp[i-1][k] - p[i] * k) q.pop_back(); q.push_back(k); } while (!q.empty() && q.front() < j - L[i]) q.pop_front(); if (!q.empty()) dp[i][j] = max(dp[i][j], dp[i-1][q.front()] + p[i] * (j - q.front())); }}
}
- POJ 1159 Palindrome
模型:最少插入多少字符使字符串变回文。
核心结论:
text
答案 = n - LCS(s, reverse(s))
多解法对比
解法 复杂度 空间
LCS + 滚动数组 O(n^2) O(n)
区间 DP O(n^2) O(n^2)
哈希 + 二分 O(n log n) O(n)
核心代码:LCS 滚动数组
cpp
string t = s;
reverse(t.begin(), t.end());
vector dp(n + 1), ndp(n + 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (si-1 == tj-1) ndpj = dpj-1 + 1;
else ndpj = max(dpj, ndpj-1);
}
swap(dp, ndp);
}
cout << n - dpn << '\n';
- POJ 3280 Cheapest Palindrome
模型:给定字符串,插入或删除字符有代价,求变回文最小代价。
状态:
text
dpij = si...j 变回文最小代价
转移
若 si == sj:
text
dpij = dpi+1j-1
否则:
text
dpij = min(
dpi+1j + costs\[i],
dpij-1 + costs\[j]
)
其中 costc = min(addc, delc)。
核心代码
cpp
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (si == sj) dpij = dpi+1j-1;
else dpij = min(dpi+1j + costs\[i],
dpij-1 + costs\[j]);
}
}
cout << dp0n-1 << '\n';
- POJ 2955 Brackets
模型:最长合法括号子序列长度。
状态:
text
dpij = si...j 中最长合法括号子序列长度
转移
若 si 与 sj 匹配:
text
dpij = max(dpij, dpi+1j-1 + 2)
枚举分割点:
text
dpij = max(dpij, dpik + dpk+1j)
核心代码
cpp
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if ((si'(' && sj')') || (si'' \&\& s\[j']'))
dpij = dpi+1j-1 + 2;
for (int k = i; k < j; k++)
dpij = max(dpij, dpik + dpk+1j);
}
}
cout << dp0n-1 << '\n';
- POJ 1141 Brackets Sequence
模型:输出最短合法括号序列。
状态:
text
dpij = si...j 最少需要添加的括号数
pathij = 分割点或匹配标记
转移
若匹配:
text
dpij = dpi+1j-1
否则枚举分割:
text
dpij = min(dpik + dpk+1j)
核心代码
cpp
void print(int i, int j) {
if (i > j) return;
if (i == j) {
if (si'(' || si')') cout << "()";
else cout << "\[\]";
return;
}
if (pathij == -1) {
cout << si;
print(i+1, j-1);
cout << sj;
} else {
int k = pathij;
print(i, k);
print(k+1, j);
}
}
- POJ 1651 Multiplication Puzzle
模型:取牌,每次取中间牌得分 ai*ak*aj,求最小总得分。
状态:
text
dpij = 将 i...j 取完只剩 i,j 的最小代价
转移
text
dpij = min(dpik + dpkj + ai*ak*aj)
核心代码
cpp
for (int len = 2; len < n; len++) {
for (int i = 0; i + len < n; i++) {
int j = i + len;
dpij = INF;
for (int k = i + 1; k < j; k++)
dpij = min(dpij,
dpik + dpkj + ai*ak*aj);
}
}
cout << dp0n-1 << '\n';
- POJ 1191 棋盘分割
模型:8×8 棋盘切 n 块,最小化均方差。
状态:
text
dpkx1y1x2y2 = 切 k 块的最小平方和
转移:枚举横切、竖切。
核心代码骨架
cpp
int dfs(int k, int x1, int y1, int x2, int y2) {
if (dpkx1y1x2y2 != -1) return dpk...;
if (k == 1) return sum2(x1,y1,x2,y2);
int ans = INF;
for (int x = x1; x < x2; x++) {
ans = min(ans, dfs(k-1, x1,y1,x,x2? ...) + sum2(...));
ans = min(ans, dfs(k-1, x+1,y1,x2,y2) + sum2(...));
}
// 同理竖切
return dpk... = ans;
}
最终答案:
text
sqrt(dpn1188 / n - avg^2)
- POJ 1160 Post Office
模型:V 个村庄建 P 个邮局,最小化总距离。
状态:
text
dpij = 前 i 个村建 j 个邮局的最小代价
costij = 在 i...j 建一个邮局的最小距离和
转移
text
dpij = min(dpkj-1 + costk+1i)
多解法对比
解法 复杂度 特点
普通区间 DP O(P V^2) 可过
四边形不等式 O(P V) 进阶
核心代码
cpp
for (int i = 1; i <= V; i++)
dpi1 = cost1i;
for (int j = 2; j <= P; j++)
for (int i = j; i <= V; i++)
for (int k = j - 1; k < i; k++)
dpij = min(dpij, dpkj-1 + costk+1i);
- POJ 3254 Corn Fields
模型:网格中选格子,相邻不能同时选,求方案数。
状态:
text
dprowstate = 第 row 行状态为 state 的方案数
合法状态:
text
!(state & (state << 1))
核心代码
cpp
for (int s = 0; s < (1<<n); s++)
if (!(s & (s<<1))) oks = true;
dp00 = 1;
for (int r = 1; r <= m; r++) {
for (int s = 0; s < (1<<n); s++) {
if (!oks || (s & fieldr)) continue;
for (int ps = 0; ps < (1<<n); ps++) {
if (s & ps) continue;
dprs = (dprs + dpr-1ps) % MOD;
}
}
}
- POJ 1185 炮兵阵地
模型:炮兵不能攻击到上下左右两格内的其他炮兵。
合法状态:
text
!(s & (s<<1)) && !(s & (s<<2))
状态:
text
dprowcurprev
核心代码骨架
cpp
for (int r = 1; r <= n; r++) {
for (int cur : states) {
if (cur & fieldr) continue;
for (int prev : states) {
if (cur & prev) continue;
for (int pprev : states) {
if ((cur & pprev) || (prev & pprev)) continue;
dprcurprev = max(dprcurprev,
dpr-1prevpprev + popcount(cur));
}
}
}
}
- POJ 2411 Mondriaan's Dream
模型:用 1×2 骨牌铺满 H×W,求方案数。
多解法对比
解法 复杂度 特点
按行状压 DP O(H * 2^W * 合法转移) 经典
轮廓线 DP O(HW * 2^W) 更通用
核心代码:按行 DP
cpp
void dfs(int col, int cur, int nxt) {
if (col == W) {
transcur.push_back(nxt);
return;
}
if (cur & (1<<col)) {
dfs(col+1, cur, nxt);
} else {
// 横放
if (col+1 < W && !(cur & (1<<(col+1))))
dfs(col+2, cur, nxt);
// 竖放
dfs(col+1, cur, nxt | (1<<col));
}
}
dp00 = 1;
for (int r = 0; r < H; r++)
for (int s = 0; s < (1<<W); s++)
for (int ns : transs)
dpr+1ns += dprs;
cout << dpH0 << '\n';
-
POJ 2282 The Counting Problem
模型:统计 a,b 中数字 0~9 出现次数。
多解法对比
解法 复杂度 特点
逐位统计 O(10 log n) 数学强
数位 DP O(10 * log n * 状态) 通用
逐位统计核心
cpp
long long countDigit(long long n, int d) {
long long cnt = 0;
for (long long base = 1; base <= n; base *= 10) {
long long high = n / (base * 10);
long long cur = (n / base) % 10;
long long low = n % base;
if (d == 0) { if (high == 0) continue; high--; cnt += high * base + low + 1; high++; cnt -= base; // 实际需按公式调整,建议直接写数位 DP } else { if (cur > d) cnt += (high + 1) * base; else if (cur == d) cnt += high * base + low + 1; else cnt += high * base; }}
return cnt;
}
0 的处理容易错,比赛建议直接写数位 DP。
-
POJ 3252 Round Numbers
模型:统计 a,b 中二进制 0 的个数不少于 1 的个数。
状态:
text
dpposcnt0cnt1leadlimit
核心代码骨架
cpp
int dfs(int pos, int c0, int c1, bool lead, bool limit) {
if (pos == -1) return c0 >= c1;
if (!limit && !lead && dpposc0c1 != -1)
return dpposc0c1;
int up = limit ? bitpos : 1;
int ans = 0;
for (int x = 0; x <= up; x++) {
if (lead && x == 0)
ans += dfs(pos-1, 0, 0, true, limit && x == up);
else
ans += dfs(pos-1, c0 + (x == 0), c1 + (x == 1),
false, limit && x == up);
}
if (!limit && !lead) dpposc0c1 = ans;
return ans;
}
答案:
text
solve(b) - solve(a-1)
- POJ 1088 滑雪
模型:网格中找最长下降路径。
多解法对比
解法 复杂度 特点
记忆化搜索 O(RC) 最好写
排序 + DP O(RC log RC) 无递归
核心代码:记忆化
cpp
int dfs(int x, int y) {
if (dpxy) return dpxy;
dpxy = 1;
for (int d = 0; d < 4; d++) {
int nx = x + dxd, ny = y + dyd;
if (in(nx,ny) && anxny < axy)
dpxy = max(dpxy, dfs(nx,ny) + 1);
}
return dpxy;
}
- POJ 2342 Anniversary party
模型:树形 DP,员工不能和直接上司同时选,最大快乐值。
状态:
text
dpu0 = 不选 u 的最大值
dpu1 = 选 u 的最大值
转移
text
dpu0 = sum(max(dpv0, dpv1))
dpu1 = valu + sum(dpv0)
核心代码
cpp
void dfs(int u, int fa) {
dpu0 = 0;
dpu1 = valu;
for (int v : gu) {
if (v == fa) continue;
dfs(v, u);
dpu0 += max(dpv0, dpv1);
dpu1 += dpv0;
}
}
答案:
cpp
cout << max(dproot0, dproot1) << '\n';
多解法对比速查表
题目 推荐解法 备选解法 关键优化
1014 二进制拆分 DFS / 单调队列 可行性背包
1742 used 数组 bitset / 单调队列 多重部分和
1276 二进制拆分 单调队列 最大可达
3017 单调队列 + multiset 线段树 分段最大值
2373 单调队列 线段树 区间限制
1821 单调队列 线段树 工人刷墙
1159 LCS 滚动 区间 DP / 哈希 回文插入
3280 区间 DP 记忆化 插入删除取小
2955 区间 DP 栈 括号匹配
1141 区间 DP + 路径 记忆化 输出方案
1651 区间 DP 记忆化 矩阵连乘
1191 记忆化区间 DP 递推 棋盘分割
1160 区间 DP 四边形不等式 邮局
3254 状压 DP 轮廓线 相邻限制
1185 状压 DP 滚动数组 三行影响
2411 按行状压 轮廓线 DP 骨牌覆盖
2282 数位 DP 逐位统计 0 特殊
3252 数位 DP 组合计数 二进制
1088 记忆化搜索 排序 DP 最长下降
2342 树形 DP 拓扑排序 上司限制