「学习笔记」DP学习笔记 1
序列DP#
一般序列 DP 核心思想:将序列的前 个数的状态用一个更简单的形式表示出,并且体现出这些状态对后续的影响。
题目#
ABC 267D
给定一个序列 ,找到一个长度为 的子序列 ,使得 最大。
。
状态::前 个数,选 个数的最大和;
转移:。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 2010;
int n, m;
ll a[N], dp[N][N];
int main() {
memset(dp, 128, sizeof dp);
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
dp[i][0] = 0;
}
dp[0][0] = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= min(i, m); ++ j) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + 1ll * j * a[i]);
}
}
printf("%lld\n", dp[n][m]);
return 0;
}
B3637 最长上升子序列
给定一个序列,求它的最长上升子序列。
状态::最长上升子序列中第 个元素(是什么);
转移:如果 新元素 大于 ,则 ,否则,二分出第一个大于等于 的前一个位置,替换上它。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
int n;
int y[100], dp[100];
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
y[i] = read();
}
dp[1] = y[1];
int cnt = 1;
for (int i = 2; i <= n; ++ i) {
if (y[i] > dp[cnt]) {
dp[++ cnt] = y[i];
}
else {
int p = lower_bound(dp + 1, dp + cnt + 1, y[i]) - dp;
dp[p] = y[i];
}
}
printf("%d\n", cnt);
return 0;
}
P1439 【模板】最长公共子序列
给定两个 的序列 ,求它们的最长公共子序列。
。
- 朴素做法 :
状态::第一个串的前 位,第二个串的前 位的 LCS 的长度;
如果当前的 相同(即是有新的公共元素) 那么dp[i][j]=max(dp[i][j],dp[i−1][j−1])+1;
;如果不相同,即无法更新公共元素,考虑继承:dp[i][j]=max(dp[i−1][j],dp[i][j−1])
。
#include<iostream>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 2010;
int n, m;
int dp[N][N], a1[N], a2[N];
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
a1[i] = read();
}
for (int i = 1; i <= m; ++ i) {
a2[i] = read();
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if (a1[i] == a2[j]) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
printf("%d\n", dp[n][m]);
return 0;
}
- 针对本题的 做法。
我们将 中的元素改为该元素在 中的位置,如果有一段位置是单调递增的,则这一段就是公共的子序列,我们再进行 DP。
状态::最长公共子序列的第 个元素(是什么);
转移:如果当前的 大于 ,则 ,否则,二分找出大于等于 的前一个位置,替换。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
int n;
ll a[N], b[N], l[N], dp[N];
int main() {
n = read();
for (int i = 1; i <= n; ++i) {
a[i] = read();
l[a[i]] = i;
}
for (int i = 1; i <= n; ++i) {
b[i] = read();
b[i] = l[b[i]];
}
int len = 1, s;
dp[1] = b[1];
for (int i = 2; i <= n; ++i) {
if (b[i] > dp[len]) {
len ++;
dp[len] = b[i];
}
else {
s = lower_bound(dp + 1, dp + len + 1, b[i]) - dp;
dp[s] = b[i];
}
}
printf("%d", len);
return 0;
}
区间DP#
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
令状态 表示将下标位置 到 的所有元素合并能获得的价值的最大值,那么 , 为将这两组元素合并起来的代价。
题目#
P1775 石子合并
有 堆石子,每堆石子有编号有重量,现在要将它们合并成一堆,只能合并相邻的两堆石子,且代价为两堆石子的重量和,求最小代价。
状态::合并 的石子的最小代价。
转移:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 410;
int n;
int a[N], dp[N][N], s[N];
int main() {
memset(dp, 0x3f, sizeof dp);
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
dp[i][i] = 0;
s[i] = s[i - 1] + a[i];
}
for (int l = 2; l <= n; ++ l) {
for (int i = 1; i + l - 1 <= n; ++ i) {
int j = i + l - 1;
for (int k = i; k < j; ++ k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1]);
}
}
}
printf("%d\n", dp[1][n]);
return 0;
}
P4170 [CQOI2007]涂色
有一条长度为 的木板,初始时没有涂过任何颜色。每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。用尽量少的涂色次数达到目标。
状态: 将木板 涂成目标颜色的最小涂色次数。
转移:
如果 ,则 ;
否则
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 100;
int n;
char s[N];
ll dp[N][N];
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; ++ i)
dp[i][i] = 1;
for (int l = 1; l < n; ++ l) {
for (int i = 1; i + l <= n; ++ i) {
int j = i + l;
if (s[i] == s[j]) {
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]);
} else {
for (int k = i; k < j; ++ k)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
printf("%lld\n", dp[1][n]);
return 0;
}
P1220 关路灯
在一条路线上安装了 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。现在已知老张走的速度为 ,每个路灯的位置(是一个整数,即距路线起点的距离,单位:)、功率(),老张关灯所用的时间很短而可以忽略不计。问:怎样最省电?
考虑对于状态的设计,关掉一段区间的灯,需要存下左右端点,需要两维,对于关掉区间 ,最后一次只可能是关掉 位置的灯或者 位置的灯,即最后停留的位置有最左端与最右端两种,且对答案有影响,再加一维来存这两种情况。
状态:
转移:),
,
初始化:。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int f = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
f |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return f ? ~x + 1 : x;
}
const int N = 110;
int n, c, sum, ans;
int a[N], w[N], s[N];
int dp[N][N][2];
bool b[N];
int main() {
n = read(), c = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read(), w[i] = read();
s[i] = s[i - 1] + w[i];
}
memset(dp, 0x3f, sizeof dp);
dp[c][c][0] = 0;
dp[c][c][1] = 0;
for (int j = c; j <= n; ++ j) {
for (int i = j - 1; i >= 1; -- i) {
dp[i][j][0] = min(dp[i + 1][j][0] + (a[i + 1] - a[i]) * (s[n] + s[i] - s[j]), dp[i + 1][j][1] + (a[j] - a[i]) * (s[n] + s[i] - s[j]));
dp[i][j][1] = min(dp[i][j - 1][0] + (a[j] - a[i]) * (s[n] + s[i - 1] - s[j - 1]), dp[i][j - 1][1] + (a[j] - a[j - 1]) * (s[n] + s[i - 1] - s[j - 1]));
}
}
ans = min(dp[1][n][0], dp[1][n][1]);
printf("%d", ans);
return 0;
}
P3146 [USACO16OPEN]248 G
给定一个 的地图,在里面玩 2048,每次可以合并相邻两个(数值范围 ),问序列中出现的最大数字的值最大是多少。注意合并后的数值并非加倍而是 ,例如 与 合并后的数值为 ,
这里要考虑 2048 的游戏规则,即只有两个相邻的数相等才能合。
状态::合并区间 后的最大数值。
转移:,条件:,这里要注意,如果 为 ,说明这段区间没被更新到,因此答案也不可能为 ,应该为 。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 510;
int n, ans;
int a[N];
int dp[N][N];
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
dp[i][i] = a[i];
}
for (int len = 1; len <= n - 1; ++ len)
for (int i = 1; i <= n - len; ++ i) {
int j = i + len;
for (int k = i; k < j; ++ k) {
if (dp[k + 1][j] > 0 && dp[i][k] == dp[k + 1][j]) {
dp[i][j] = max(dp[i][j], dp[k + 1][j] + 1);
ans = max(ans, dp[k + 1][j] + 1);
}
}
}
printf("%d", ans);
return 0;
}
P3205 [HNOI2010]合唱队
合唱队一共 个人,第 个人的身高为 米(),并已知任何两个人的身高都不同。假定最终排出的队形是 个人站成一排,为了简化问题,小 A 想出了如下排队的方式:他让所有的人先按任意顺序站成一个初始队形,然后从左到右按以下原则依次将每个人插入最终棑排出的队形中: 第一个人直接插入空的当前队形中;对从第二个人开始的每个人,如果他比前面那个人高( 较大),那么将他插入当前队形的最右边。如果他比前面那个人矮( 较小),那么将他插入当前队形的最左边。当 个人全部插入当前队形后便获得最终排出的队形。答案要对 取模,,。
这道题与关路灯那道题差不多,在状态设计上要考虑上一个被插入的人是插入的左边还是右边。
状态::区间 中,最后一个人被插入了 左边 / 右边。
转移:
如果 ,那么 ,
如果 ,那么 ,
如果 ,那么 。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 2010;
const int mod = 19650827;
int dp[N][N][2], a[N];
int main() {
int n;
n = read();
for (int i = 1; i <= n; ++ i)
a[i] = read();
for (int i = 1; i <= n; ++ i)
dp[i][i][0] = 1;
for (int i = n - 1; i >= 1; -- i) {
for (int j = i + 1; j <= n; ++ j) {
if (a[i] < a[j]) {
dp[i][j][0] += dp[i + 1][j][1];
dp[i][j][1] += dp[i][j - 1][0];
dp[i][j][0] %= mod;
dp[i][j][1] %= mod;
}
if (a[i] < a[i + 1]) {
dp[i][j][0] += dp[i + 1][j][0];
dp[i][j][0] %= mod;
}
if (a[j] > a[j - 1]) {
dp[i][j][1] += dp[i][j - 1][1];
dp[i][j][1] %= mod;
}
}
}
printf("%d", (dp[1][n][0] + dp[1][n][1]) % mod);
return 0;
}
P5336 [THUSC2016]成绩单
L 老师共有 份成绩单,其中编号为 的的成绩单分数为 。发放成绩单时,L 老师会从当前的一叠成绩单中抽取连续的一段,让这些同学来领取自己的成绩单。当这批同学领取完毕后,L 老师再从剩余的成绩单中抽取连续的一段,供下一批同学领取。经过若干批次的领取后,成绩单将被全部发放到同学手中。不能让分数相差太远的同学在同一批领取成绩单;尽量减少领取成绩单的批次数。对于一个分发成绩单的方案,我们定义其代价为:,其中 是分发的批次数,对于第 披分发的成绩单, 是最高分数, 是最低分数, 和 是给定的评估参数,求最小代价。
,,,。
区间DP,设 为区间 全部选择的最小代价。
但是,我们发现,影响我们最终答案的不只是 ,还有剩余部分的最大值和最小值,所以还要再加两维。
为了方便,我们直接新开一个数组 ,表示在区间 选完后剩余部分的最大值为 ,最小值为 的最小代价。
由于 很小,但 ,所以需要离散化。
转移:
,
,
,
当 时,还要注意初始化,设置好边界以防越界。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 60;
int n, a, b, m;
int w[N], v[N];
int ans[N][N], dp[N][N][N][N];
void dfs(int l, int r) {
if (l > r) return ;
if (ans[l][r]) return ;
if (l == r) {
ans[l][r] = a;
for (int i = 1; i <= w[l]; ++ i) {
for (int j = i; j <= w[l] - 1; ++ j) {
dp[l][r][i][j] = 1e9 + 5;
}
}
for (int i = w[l] + 1; i <= m; ++ i) {
for (int j = i; j <= m; ++ j) {
dp[l][r][i][j] = 1e9 + 5;
}
}
return ;
}
ans[l][r] = 1e9 + 5;
for (int i = 1; i <= m; ++ i) {
for (int j = i; j <= m; ++ j) {
int res = 1e9 + 5;
for (int k = l; k < r; ++ k) {
dfs(l, k), dfs(k + 1, r);
res = min(res, ans[l][k] + dp[k + 1][r][i][j]);
res = min(res, dp[l][k][i][j] + ans[k + 1][r]);
res = min(res, dp[l][k][i][j] + dp[k + 1][r][i][j]);
}
dp[l][r][i][j] = res;
ans[l][r] = min(ans[l][r], res + a + b * (v[j] - v[i]) * (v[j] - v[i]));
}
}
}
int main() {
n = read();
a = read(), b = read();
for (int i = 1; i <= n; ++ i) {
v[i] = w[i] = read();
}
sort(v + 1, v + n + 1);
m = unique(v + 1, v + n + 1) - v - 1;
for (int i = 1; i <= n; ++ i) {
w[i] = lower_bound(v + 1, v + m + 1, w[i]) - v;
}
dfs(1, n);
printf("%d\n", ans[1][n]);
return 0;
}
环状DP#
在环上的 DP,基本方法就是断环为链,然后把它作为区间 DP 或 序列 DP 去做。
作为区间 DP 做时有一个小技巧就是将 这个链复制两遍,以防止首位的一些非法情况被我们计算。
P1880 [NOI1995] 石子合并
石子合并,但是,是在环上。
断环为链,复制两遍这条链转化为区间 DP。
状态::区间 的最大得分,:区间 的最小得分。
转移:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
ll r[600], g[600];
ll f1[600][600], f2[600][600];
int main() {
int n;
n = read();
for (int i = 1; i <= n; ++i) {
r[i] = read();
r[i + n] = r[i];
g[i] = g[i - 1] + r[i];
f1[i][i] = f2[i][i] = 0;
}
for (int i = n + 1; i <= 2 * n; ++i) {
f2[i][i] = 0;
g[i] = g[i - 1] + r[i];
}
for (int l = 1; l < n; ++l) {
for (int i = 1, j = i + l; i < n * 2 && j <= n * 2; ++i, j = i + l) {
f2[i][j] = 100000000;
for (int k = i; k < j; ++k) {
f1[i][j] = max(f1[i][j], f1[i][k] + f1[k + 1][j] + g[j] - g[i - 1]);
f2[i][j] = min(f2[i][j], f2[i][k] + f2[k + 1][j] + g[j] - g[i - 1]);
}
}
}
ll maxn = 0, minn = 1e18;
for (int i = 1; i <= n; ++i) {
maxn = max(maxn, f1[i][i + n - 1]);
minn = min(minn, f2[i][i + n - 1]);
}
printf("%lld\n%lld", minn, maxn);
return 0;
}
P1063 [NOIP2006 提高组] 能量项链
给定一个有 个珠子的项链,每个珠子有头标记和尾标记,相邻两个珠子,前一个珠子的尾标记等于后一个珠子的头标记,只有相邻的两个柱子能合并成一个,并产生能量,能量为 ,合并后的新珠子,头标记等于前一颗珠子的头标记,尾标记等于后一颗珠子的尾标记。求最大能量。
很经典的环形 DP 题。
状态:: 合并区间 释放的最大能量。
转移:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
int n;
ll a[300], f[300][300];
ll maxn;
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
a[i + n] = a[i];
}
for (int i = 2; i < 2 * n; ++ i)
for (int j = i - 1; i - j < n && j >= 1; -- j)
for (int k = j; k < i; ++ k) {
f[j][i] = max(f[j][i], a[j] * a[k + 1] * a[i + 1] + f[j][k] + f[k + 1][i]);
if (f[j][i] > maxn) maxn = f[j][i];
}
printf("%lld", maxn);
return 0;
}
作者:yifan0305
出处:https://www.cnblogs.com/yifan0305/p/17466396.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
转载时还请标明出处哟!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!