P1220 关路灯
思路:
令
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示区间
[
i
[i
[i ~
j
]
j]
j] 内的灯已经被打开时所耗费的最小功率。那么在更新这个区间时,由于可以折返去关闭另头的灯,当前状态无法表示折返这一操作,所以应当对其分类讨论。
- 老王在位置 [ i + 1 ] [i + 1] [i+1] 关闭了位置 [ i ] [i] [i] 的灯
- 老王在位置 [ j ] [j] [j] 关闭了位置 [ i ] [i] [i] 的灯
- 老王在位置 [ j − 1 ] [j - 1] [j−1] 关闭了位置为 [ j ] [j] [j] 的灯
- 老王在位置 [ i ] [i] [i] 关闭了位置为 [ j ] [j] [j] 的灯
所以我们再设一个状态,为
f
[
i
]
[
j
]
[
0
/
1
]
f[i][j][0/1]
f[i][j][0/1] [0] 表示当前老王位于区间端点
i
i
i 处,
[
1
]
[1]
[1] 表示当前位于区间端点
[
j
]
[j]
[j] 处,所以根据上述讨论,状态转移方程为:
更新端点
[
i
+
1
]
[i + 1]
[i+1] 为
[
i
]
[i]
[i] 时,可以是上述的 情况
1
1
1 或者情况
2
2
2
f[i][j][0] = min(f[i + 1][j][0] + (a[i + 1] - a[i]) * (p[i] + p[n] - p[j]), f[i + 1][j][1] + (a[j] - a[i]) * (p[i] + p[n] - p[j]));
更新端点
[
j
−
1
]
[j -1]
[j−1] 为
[
j
]
[j]
[j] 时,可以是上述的 情况
3
3
3 或者情况
4
4
4
f[i][j][1] = min(f[i][j - 1][1] + (a[j] - a[j - 1]) * (p[i - 1] + p[n] - p[j - 1]), f[i][j - 1][0] + (a[j] - a[i]) * (p[i - 1] + p[n] - p[j - 1]));
其中p[i] 为路灯功率的前缀和
初始化:因为是求最小值,所有情况初始化为最大值,老王最初所在的路灯可以直接被关掉,所以初始
f
[
c
]
[
c
]
[
0
]
=
f
[
c
]
[
c
]
[
1
]
f[c][c][0] = f[c][c][1]
f[c][c][0]=f[c][c][1]
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
int a[N], p[N];
int f[N][N][2]; //区间(i, j) [1]表示在区间又右边端点 [0]表示在区间左端点
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n, c;
cin >> n >> c;
for(int i = 1; i <= n; i ++) {
cin >> a[i] >> p[i];
p[i] += p[i - 1];
}
memset(f, 0x3f, sizeof f);
f[c][c][0] = f[c][c][1] = 0;
for(int l = 2; l <= n; l ++ ) {
for(int i = 1; i + l - 1 <= n; i ++) {
int j = i + l - 1;
f[i][j][0] = min(f[i + 1][j][0] + (a[i + 1] - a[i]) * (p[i] + p[n] - p[j]),
f[i + 1][j][1] + (a[j] - a[i]) * (p[i] + p[n] - p[j]));
f[i][j][1] = min(f[i][j - 1][1] + (a[j] - a[j - 1]) * (p[i - 1] + p[n] - p[j - 1]),
f[i][j - 1][0] + (a[j] - a[i]) * (p[i - 1] + p[n] - p[j - 1]));
}
}
cout << min(f[1][n][0], f[1][n][1]) << '\n';
}
P3205 [HNOI2010]合唱队
思路:
同上题,该题也可以从左边或者右边加入数字,需要表示所有状态的话,需要用
f
[
i
]
[
[
j
]
[
0
/
1
]
f[i][[j][0/1]
f[i][[j][0/1] 表示区间
[
i
[i
[i ~
j
]
j]
j] 已经排好,并且最后插入的数字是从左边
[
0
]
[0]
[0] 或者 右边
[
1
]
[1]
[1] 插入的
分类讨论:
可以从左侧插入的情况:
- 上一个数字从左侧插入 并且
a
[
i
]
<
a
[
i
+
1
]
a[i] < a[i + 1]
a[i]<a[i+1]
f[i][j][0] += f[i + 1][j][0]
- 上一个数字从右侧插入 并且
a
[
i
]
<
a
[
j
]
a[i] < a[j]
a[i]<a[j]
f[i][j][0] += f[i + 1][j][1]
可以从右侧插入的情况:
3. 上一个数字从左侧插入且
a
[
i
]
<
a
[
j
]
a[i] < a[j]
a[i]<a[j] f[i][j][1] += f[i][j - 1][0]
4. 上一个数字从右侧插入且
a
[
j
−
1
]
<
a
[
j
]
a[j - 1] < a[j]
a[j−1]<a[j] f[i][j][1] += f[i][j - 1][1]
初始化:
只有一个数字时,从左侧插入和从右侧插入是同一种情况,如果初始化为
f
[
i
]
[
j
]
[
0
]
=
f
[
i
]
[
j
]
[
1
]
=
1
f[i][j][0] = f[i][j][1] = 1
f[i][j][0]=f[i][j][1]=1 这样是将一种统计为两种,所以只需呀初始化其中任意一个为
1
1
1 即可
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2010, mod = 19650827;
int f[N][N][2]; //(i, j) [0] 表示从左边放入 [1]表示从右边放入 比较上一个放入与这个放入的数的大小
int a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n;
cin >> n;
memset(f, 0, sizeof f);
for(int i = 1; i <= n; i ++) {
cin >> a[i];
f[i][i][0] = 1;
}
for(int l = 2; l <= n; l ++) {
for(int i = 1; i + l - 1 <= n; i ++) {
int j = i + l - 1;
if(a[i] < a[i + 1]) f[i][j][0] += f[i + 1][j][0];
if(a[i] < a[j]) f[i][j][0] += f[i + 1][j][1];
if(a[j] > a[i]) f[i][j][1] += f[i][j - 1][0];
if(a[j] > a[j - 1]) f[i][j][1] += f[i][j - 1][1];
f[i][j][0] %= mod;
f[i][j][1] %= mod;
}
}
cout << (f[1][n][0] + f[1][n][1]) % mod << '\n';
}
P1880 [NOI1995] 石子合并
很经典的区间
D
p
Dp
Dp 例题
思路:
用
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示区间
[
i
[i
[i ~
j
]
j]
j] 的数字已经合并好时的最大(小)价值,当我们枚举区间长度为
3
3
3 及以上时,我们并不知道是用前面几个数合并后的结果去合并后面的数得到的结果最优(比如长度为
3
3
3 是用前两个数合并最后一个数还是用第一个数合并最后两个数) ,所以我们需要枚举一个中间量
k
k
k ,来表示应当是哪个区间与哪个区间合并。
合并一个区间的代价就是这两个区间的值的总和,所以需要预处理一个前缀和
状态转移方程(以求最大值为例):
合并两个区间可以表示为把两个已经合并好的区间再次合并,可以表示为:
f[i][j] = max(f[i][k] + f[k + 1][j] + s[j] - s[i - 1])
s[i] 为石子值的前缀和
预处理:
长度为
1
1
1 的区间合并代价为
0
0
0 预处理所有 f[i][i] = 0
因为本题是一个环形的石子合并,只需将当前长度为
n
n
n 的所有数复制一份接成为长度为
2
n
2n
2n 的区间,这样操作之后,在这个
2
n
2n
2n 的区间上找一个长度为
n
n
n 的区间的最优解就是最终答案
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110 * 2;
int f_max[N][N], f_min[N][N];
int a[N], s[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i], a[i + n] = a[i];
for(int i = 1; i <= 2 * n; i ++) s[i] = s[i - 1] + a[i];
memset(f_max, -0x3f, sizeof f_max);
memset(f_min, 0x3f, sizeof f_min);
for(int i = 1; i <= 2 * n; i ++) f_max[i][i] = 0, f_min[i][i] = 0;
for(int l = 2; l <= n; l ++) {
for(int i = 1; i + l - 1 <= 2 * n; i ++) {
int j = i + l - 1;
for(int k = i; k <= j; k ++) {
f_max[i][j] = max(f_max[i][j], f_max[i][k] + f_max[k + 1][j] + s[j] - s[i - 1]);
f_min[i][j] = min(f_min[i][j], f_min[i][k] + f_min[k + 1][j] + s[j] - s[i - 1]);
}
}
}
int ans_min = 0x3f3f3f3f, ans_max = 0;
for(int i = 1; i <= n; i ++) {
ans_min = min(ans_min, f_min[i][i + n - 1]);
ans_max = max(ans_max, f_max[i][i + n - 1]);
}
cout << ans_min << '\n' << ans_max << '\n';
}
P1063 [NOIP2006 提高组] 能量项链
思路:
本题和石子合并很像,多了一些细节问题,我们还是令
f
[
i
]
[
j
]
f[i][j]
f[i][j] 为区间
[
i
[i
[i ~
j
]
j]
j] 合并后得到的最大值,然后就需要枚举是区间里哪两部分进行合并,枚举一个
k
k
k 在枚举的时候,因为除了最后一次合并,每一次都是三个不一样的数进行合并,所以
k
k
k 不能等于区间的左右端点。又因为需要一次性选择三个数字,所以区间长度需要为
3
3
3 的时候才可以合并;因为这题是环形的合并,实现环形合并的效果只需将所有数字复制一次然后全部接在最后一个数之后,枚举的时候限制区间长度即可。在最后一次合并时,会将某个数自己与自己相乘一次;所以本题限制的区间长度应当是到
n
+
1
n + 1
n+1
状态转移方程:
f[i][j] = max(f[i][k] + f[k][j] + a[i] * a[k] * a[j])
代码
/*
与石子合并相比,石子合并枚举的是 [1, 2] [3] 合并还是 [1] [2, 3] 合并, 所以 k 可以等于(i|j)
该题画出状态后应当知道至少区间长度为 3,才可以进行合并 [1] [2] [3] 三者合并 因而 k != i && k != j
并且最后一次合并出的数字应当是 [1] [2] [1] 即自己与自己合并,所以区间长度应为 (n + 1)
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 110 * 2;
int f[N][N];
int a[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n;
cin >> n;
for(int i = 1 ;i <= n; i ++) cin >> a[i], a[i + n] = a[i];
for(int l = 2; l <= n + 1; l ++) {
for(int i = 1; i + l - 1 <= 2 * n; i ++) {
int j = i + l - 1;
for(int k = i + 1; k <= j - 1; k ++) {
f[i][j] = max(f[i][j], f[i][k] + f[k][j] + a[i] * a[k] * a[j]);
}
}
}
int ans = 0;
for(int i = 1; i <= n; i ++) {
ans = max(ans, f[i][i + n]);
}
cout << ans << '\n';
}
P3146 [USACO16OPEN]248 G
思路:
可以看作有条件的石子合并问题,令
f
[
i
]
[
j
]
f[i][j]
f[i][j] 为区间
[
i
[i
[i ~
j
]
j]
j] 合并后能产生的最大数字。在一个区间
[
i
[i
[i ~
j
]
j]
j] 内对于哪两部分合并之后能产生最大值未知,所以需要枚举一个中间量
k
k
k 来枚举合并方式。本题需要两个相邻的数相等才能够进行合并,所有本质上只是在石子合并上加了一个限制 只有f[i][k] == f[k + 1][j]
时才能让这两个区间的值合并。
初始化:不进行任何合并能得到的最大值是自己本身 f[i][i] = a[i]
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int a[N], f[N][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
int ans = 0;
for(int i = 1; i <= n; i ++) f[i][i] = 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 ++) {
if(f[i][k] == f[k + 1][j]) {
f[i][j] = max(f[i][j], f[i][k] + 1);
ans = max(ans, f[i][j]);
}
}
}
}
cout << ans << '\n';
return 0;
}
P4170 [CQOI2007]涂色
思路:
令
f
[
i
]
[
j
]
f[i][j]
f[i][j] 为给区间
[
i
[i
[i ~
j
]
j]
j] 上色所需要的最小次数。在更新区间时候,从
f
[
i
+
1
]
[
j
]
f[i + 1][j]
f[i+1][j] 更新到
f
[
i
]
[
j
]
f[i][j]
f[i][j] 时,如果
[
i
]
[i]
[i] 处的颜色与
[
j
]
[j]
[j] 处的颜色相同的话,那么这次更新所需涂色的次数应为
0
0
0,如果颜色不同的话,同样我们也是不知道先合并哪两个区间所得到的,枚举一个
k
k
k 用于枚举区间内哪两个区间合并后能得到最优值。
初始化:
对于每一个格子,给自己上色至少需要一次:f[i][i] = 1
状态转移方程:
f[i][j] = min(f[i + 1][j], f[i][j - 1] (颜色相同)
f[i][j] = min(f[i][k] + f[k + 1][j]) (颜色不同)
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
int f[N][N];
char s[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
scanf("%s", s + 1);
int n = strlen(s + 1);
memset(f, 0x3f, sizeof f);
for(int i = 1; i <= n; i ++) f[i][i] = 1;
for(int l = 2; l <= n; l ++) {
for(int i = 1; i + l - 1<= n; i ++) {
int j = i + l - 1;
if(s[i] == s[j]) { //颜色相同
f[i][j] = min(f[i + 1][j], f[i][j - 1]);
} else { // 颜色不同 从哪分隔比较好
for(int k = i; k < j; k ++) {
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);
}
}
}
}
cout << f[1][n] << '\n';
}
CF607B Zuma
思路:
令
f
[
i
]
[
j
]
f[i][j]
f[i][j] 为区间
[
i
[i
[i ~
j
]
j]
j] 被消除所需要的最少次数。
当枚举到的区间两个端点相等时 可以更新: f[i][j] = f[i + 1][j - 1]
当两个端点不同时,枚举一个
k
k
k 来枚举区间内哪两个部分合并得到的值最优
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);
在去更新回文串长度为 2 2 2 时, f [ i ] [ i + 1 ] f[i][i + 1] f[i][i+1] 在进行上述操作后会变成 f [ i + 1 ] [ i ] f[i + 1][i] f[i+1][i] 没有任何变化,并不是从长度为1更新,所以需要单独预处理。
预处理:
对于每一个数字,都至少需要一次来移除它 f[i][i] = 1
对于长度为 2 的区间,如果相邻两个数相等,那么同时移除这两个数只需一次 f[i][i + 1] = 1 + (a[i] != a[i + 1])
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 510;
int a[N], f[N][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
#ifndef ONLINE_JUDGE
freopen("D:/Cpp/program/Test.in", "r", stdin);
freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
memset(f, 0x3f, sizeof f);
for(int i = 1; i <= n; i ++) f[i][i] = 1;
for(int i = 1; i < n; i ++) {
if(a[i] == a[i + 1]) f[i][i + 1] = 1;
else f[i][i + 1] = 2;
}
for(int l = 3; l <= n; l ++) {
for(int i = 1; i + l - 1 <= n; i ++) {
int j = i + l - 1;
if(a[i] == a[j]) f[i][j] = f[i + 1][j - 1];
for(int k = i; k < j; k ++) {
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);
}
}
}
cout << f[1][n] << '\n';
}