技巧总结
习题
dls动态规划中级课
石子合并变形---ICPC Beijing 2017 J, Pangu and Stones
石子合并模型,限制每次合并只能合并连续的 \([L, R]\) 堆石子,询问最小的合并代价,如果没法操作,输出 0
思路
- 普通石子合并状态无法完成,考虑多加一维状态: \(f[l][r][k]\) 将 [l, r] 合并成 k 堆石子最小代价。
- 对于上述状态很朴素的想法是枚举分界点,然后枚举分界点左右的石子堆个数,由于还要枚举 k 这一维,所以复杂度是 \(O(n^5)\) 无法通过。
- 考虑优化一维枚举,其实发现,枚举左右连续合并石子堆的个数,其实就是枚举左边合并成 1 堆,右边合并成 k - 1 堆,这样的枚举可以不重不漏。
- 最后注意边界,
f[i][i][1] = 0
,其他赋值正无穷
const int N = 110;
int f[N][N][N], a[N], s[N];
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
IOS;
int n, L, R;
while (cin >> n >> L >> R) {
for (int i = 1; i <= n; i++) cin >> a[i], s[i] = s[i - 1] + a[i];
for (int l = 1; l <= n; l++) {
for (int r = l; r <= n; r++)
for (int k = 0; k <= n; k ++)
f[l][r][k] = 1 << 29;
}
for (int len = 1; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l ++) {
int r = l + len - 1;
if (l == r) {
f[l][r][1] = 0;
continue;
}
for (int k = 2; k <= len; k ++) {
for (int mid = l; mid < r; mid ++) {
f[l][r][k] = min(f[l][r][k], f[l][mid][1] + f[mid + 1][r][k - 1]);
}
if (k >= L && k <= R) // 可以被合并为一堆
f[l][r][1] = min(f[l][r][1], f[l][r][k]);
}
f[l][r][1] += s[r] - s[l - 1];
}
}
if (f[1][n][1] >= 1 << 29) f[1][n][1] = 0;
cout << f[1][n][1] << endl;
}
// fclose(stdin);
// fclose(stdout);
return 0;
}
2020-ICPC-KunMing C, Citys
给定序列 a,每次选取连续的相同一段染成其他颜色,询问将全序列染成相同颜色的最小代价。
思路
- 贪心的发现,最小代价与,每次染色左右两端是否相同有关,每有一堆就会让代价 - 1,原总的代价是 n - 1 次。
- 可以设 \(dp[l][r]\) 为 [l, r] 内减小代价的最大值,也可以设 \(dp[l][r]\) 为染色 [l, r] 相同的最小代价。
- 对于前者就是代码中的转移,\(dp[l][r] = max(dp[l + 1][r], max\{dp[l + 1][x - 1] + dp[x][r] + 1\})\)
- 对于后者,感觉更容易考虑:
- 朴素转移 \(dp[l][r] = min(dp[l + 1][r] + 1, dp[l][r - 1] + 1)\)
- 如果 \(a[l] = a[r]\),则 \(dp[l][r] = min(dp[l][r], dp[l + 1][r - 1] + 1)\)
- 如果在 (l, r) 内存在 k,满足 a[k] = a[r] ,那么 \(dp[l][r] = min(dp[l][r], dp[k + 1][r - 1] + dp[l][k] + 1)\)
const int N = 5010;
int a[N], f[N][N], pos[N], nxt[N];
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
int T;
re(T);
while (T--) {
int n;
re(n);
for (int i = 0; i <= n; i ++)
pos[i] = n + 1;
for (int i = 1; i <= n; i ++) {
re(a[i]);
if (i != 1 && a[i] == a[i - 1]) {
i--, n--;
continue;
}
}
for (int i = n; i >= 1; i--) {
nxt[i] = pos[a[i]];
pos[a[i]] = i;
}
for (int l = 1; l <= n; l++)
for (int r = l; r <= n; r++)
f[l][r] = 0;
for (int len = 1; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
int x = nxt[l];
f[l][r] = f[l + 1][r];
while (x <= r) {
f[l][r] = max(f[l][r], f[l + 1][x - 1] + f[x][r] + 1);
x = nxt[x];
}
}
}
printf("%d\n", n - 1 - f[1][n]);
}
// fclose(stdin);
// fclose(stdout);
return 0;
}
CSP2021-S-括号序列
题意略;
思路
- 细节很多的一道题,只要思考清楚还是可以。
- 状态定义:
- \(f[l][r]\) 将 [l,r] 合法括号序列方案数
- \(g[l][r]\) 一定是 l 与 r 匹配的合法括号序列方案数,用于判重
- \(h[l][r]\) 形如
SA
这样的方案数(不是合法的,用于优化)
- Case1:
()
,(S)
,很容易考虑 - Case2:
AB
,ASB
- \(s[l]\) 必须是
'('
因为合法括号序列左右一定是括号 - 枚举前面的 [l,mid] 为 A。
AB
由 \(f[mid + 1][r]\) 转移ASA
由 \(h[mid + 1][r]\) 转移- 以上转移需要乘 \(g[l][mid]\)
- \(s[l]\) 必须是
- Case3:
(A)
,(SA)
,(AS)
。(A)
直接由 \(f[l+1][r-1]\) 转移(SA)
由 \(h[l+1][r-1]\) 转移(AS)
枚举后缀 [i, r - 1] 为'*'
,由 \(f[l + 1][i - 1]\) 转移
const int N = 510, mod = 1e9 + 7;
int f[N][N], g[N][N], h[N][N], n, k;
char s[N];
void add(int &a, int b) {
a += b;
if (a >= mod) a %= mod;
}
bool match(int i, char c) {
return s[i] == '?' || s[i] == c;
}
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
re(n), re(k);
scanf("%s", s + 1);
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
if (match(l, '(') && match(r, ')')) {
if (len == 2) {
g[l][r] = 1;
}
else {
// (A)
add(g[l][r], f[l + 1][r - 1]);
// (S)
if (r - l - 1 <= k) {
bool ok = true;
for (int i = l + 1; i < r && i <= l + k; i++) {
if (!match(i, '*')) {
ok = false;
break;
}
}
if (ok) add(g[l][r], 1);
}
// (AS)
for (int i = r - 1; i >= r - k && i > l + 1; i--) {
if (!match(i, '*')) break;
add(g[l][r], f[l + 1][i - 1]);
}
// (SA)
add(g[l][r], h[l + 1][r - 1]);
}
}
f[l][r] = g[l][r];
if (match(l, '(')) {
for (int mid = l; mid < r; mid ++) {
int tmp = 0;
// AB
add(tmp, f[mid + 1][r]);
// ASB
add(tmp, h[mid + 1][r]);
add(f[l][r], 1ll * g[l][mid] * tmp % mod);
}
}
for (int i = l; i < r - 1 && i <= l + k - 1; i++) {
if (!match(i, '*')) break;
add(h[l][r], f[i + 1][r]);
}
// printf("%d %d %d %d\n", l, r, f[l][r], g[l][r]);
}
}
printf("%d\n", f[1][n]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
ICPC CERC 2014 L, Outer space invaders
思路
- 经典变形区间DP,直接从题上莽区间DP不好做状态,问题明显可以数形结合
- 从极端情况入手,就是对于某一时刻的,要消灭完,必然要消灭最大的,然后呢,你消灭最大的,同一时刻小于他的都会被消灭。之后问题变成了 \([1,x-1]\), \([x+1, n]\) 的子问题。
- 上面的子问题显然是有最优子结构,然后就区间dp,设计状态 \(dp[l][r]\) 消灭 [l,r] 区间内完整出现的怪物最小代价,
- 注意是完整出现,因为我们递归子问题之后,子问题需要解决的是在两个子区间内完整出现的怪物,如果有在 x 点出现就被消灭了。
- 然后就很好写,这里写的记忆化搜索。复杂度 \(O(n^3)\)
vector<int> alls;
const int N = 610;
int dp[N][N], a[N], b[N], n, m, d[N];
int find(int x) {
return lower_bound(ALL(alls), x) - alls.begin() + 1;
}
int dfs(int l, int r) {
if (dp[l][r] != -1) return dp[l][r];
int &ans = dp[l][r];
if (l > r) return ans = 0;
ans = 2e9;
int mxd = -1, pos = 0;
for (int i = 0; i < n; i ++) {
if (d[i] > mxd && a[i] >= l && b[i] <= r) {
mxd = d[i];
pos = i;
}
}
if (mxd == -1) return ans = 0;
for (int mid = a[pos]; mid <= b[pos]; mid ++) {
ans = min(ans, dfs(l, mid - 1) + dfs(mid + 1, r));
}
ans += mxd;
return ans;
}
void solve() {
re(n);
alls.clear();
for (int i = 0; i < n; i ++) {
re(a[i]), re(b[i]), re(d[i]);
alls.pb(a[i]), alls.pb(b[i]);
}
sort(ALL(alls));
alls.erase(unique(ALL(alls)), alls.end());
for (int i = 0; i < n; i ++)
a[i] = find(a[i]), b[i] = find(b[i]);
m = alls.size();
for (int i = 1; i <= m; i ++)
for (int j = 1; j <= m; j++)
dp[i][j] = -1;
dfs(1, m);
printf("%d\n", dp[1][m]);
}
int main() {
int T;
re(T);
while (T--) {
solve();
}
return 0;
}
标签:int,mid,---,枚举,add,dp,freopen,DP
From: https://www.cnblogs.com/Roshin/p/remake_Interval_DP.html