YBTOJ 5.2区间DP

A.石子合并

image
image

很经典的区间 \(DP\) 模板题
我们设 \(f[l][r]\) 表示把 \(\left[l, r\right]\) 这段区间的最小/大得分
考虑枚举 \(\left[l, r\right]\) 之间的断点 \(k\)\(f[l][r] = max/min(f[l][k] + f[k][r])\)
但是由于这题是要考虑 我们考虑把原队列复制一份放在末尾
然后枚举一个长度为 \(n\) 的定区间

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 201;
int a[N], dpmax[N][N], dpmin[N][N];
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i++) a[i + n] = a[i];

    for (int len = 2; len <= n; len++) {
        for (int i = 1; i <= 2 * n + 1 - len; i++) {
            dpmin[i][i + len - 1] = 0x7fffffff;
            for (int k = 0; k < len - 1; k++) {
                dpmax[i][i + len - 1] =
                    max(dpmax[i][i + k] + dpmax[i + k + 1][i + len - 1], dpmax[i][i + len - 1]);
                dpmin[i][i + len - 1] =
                    min(dpmin[i][i + k] + dpmin[i + k + 1][i + len - 1], dpmin[i][i + len - 1]);
            }
            for (int k = i; k <= i + len - 1; k++) {
                dpmax[i][i + len - 1] += a[k];
                dpmin[i][i + len - 1] += a[k];
            }
        }
    }

    int maxn = 0, minn = 0x7fffffff;
    for (int i = 1; i <= 2 * n + 1 - n; i++) {
        maxn = max(maxn, dpmax[i][i + n - 1]);
        minn = min(minn, dpmin[i][i + n - 1]);
    }

    printf("%d\n%d", minn, maxn);

    return 0;
}

B.木板涂色

image
image

因为数据范围比较美丽 并且涂色是一个区间的操作
所以可以考虑一下区间 \(DP\)
\(f[l][r]\) 表示把 \(\left[l, r\right]\) 刷到目标状态的最小次数花费
考虑转移 还是有 \(f[l][r] = min(f[l][k] + f[k][r])\)
但问题在于 题目给的颜色没用上
发现如果 \(l\)\(r\) 颜色相同 只需要刷一次
所以有 \(f[l][r] = min(f[l + 1][r], f[l][r - 1])\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 52;
int f[N][N];
string s;

int main() {
	cin >> s;
	int n = s.length();
	memset(f, 0x3f, sizeof f );
	for (int i = 0; i < n; ++i) f[i][i] = 1;
	for (int len = 2; len <= n; ++len) {
		for (int l = 0; l + len - 1 < n; ++l) {
			int r = l + len - 1;
			if (s[l] == s[r])
				f[l][r] = min(f[l][r - 1], f[l + 1][r]);
			else {
				for (int k = l; k < r; ++k)
					f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
			}
		}
	}
	
	printf("%d",f[0][n - 1]);
	
	return 0;
}

C.消除木块

image
image

头疼
考虑区间 \(DP\)\(f[l][r]\) 表示把 \(\left[l, r\right]\) 区间内所有木块都消除的最大得分
但是转移不太好想 单纯枚举 \(k\) 的话还是那个问题:颜色是干什么用的
并且假如说我把 \(r\) 单独炸掉 那如果 \(r\) 后面还有相同颜色的拼在一起 显然就不合法了
那么我们考虑设 \(f[l][r][x]\) 表示连接 \(r\) 后面还有多少同色木块
那么显然有一个决策是直接把 \(r\) 和它连着的那一段全炸掉
或者考虑枚举 \(\left[l, r\right]\) 之间的 \(k\)
但是要有一些特点 比如说...与 \(r\) 颜色相同?
那我们把 \(\left[k + 1, r - 1\right]\) 这段崩掉
\(r\)\(k\) 就拼在一起了
具体转移可以使用记搜:

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 301;
int f[N][N][N];
int co[N], len[N], a[N];
int T, n;

int jyhss(int l, int r, int k) {
	int all = len[r] + k;
	if (l == r) return all * all;
	if (f[l][r][k] != -1) return f[l][r][k];
	int res = jyhss(l, r - 1, 0) + all * all;
	for (int kkk = l; kkk < r; ++kkk) {
		if (co[kkk] == co[r])
			res = max(res, jyhss(l, kkk, all) + jyhss(kkk + 1, r - 1, 0));
	}
	return f[l][r][k] = res;
}

int main() {
	scanf("%d", &T);
	for (int ii = 1; ii <= T; ++ii) {
		scanf("%d", &n);
		for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
		int lst = -1, cnt = 0;
		for (int i = 1; i <= n; ++i) {
			if (lst != a[i]) {
				++cnt;
				len[cnt] = 1;
				co[cnt] = a[i];
				lst = a[i];
			} else ++len[cnt];
		}
//		for (int i = 1; i <= cnt; ++i) cout<<i<<" "<<co[i]<<" "<<len[i]<<endl;
		memset(f, -1, sizeof f );
		printf("Case %d: %d\n",ii,jyhss(1, cnt, 0));
	}
	
	return 0;
}

D.棋盘分割

image
image

二维区间 \(DP\)
式子看起来很吓人 但是考虑转化一下
首先根号是无所谓的 我们让根号里的东西尽可能大就行
除以 \(n\) 当然也是无所谓的 因为 \(n\) 是定值
进一步观察 发现 \(\bar{x}\) 也是无所谓的 因为它等于全图中所有数的和除以 \(n\)
而上面打开就是 \(\sum\limits_{i=1}^n x_i^2 + \bar{x}^2 - 2 * \bar{x} * x_i\)
它等于 \(n * \bar{x}^2 - 2 * \bar{x} * \sum\limits_{i=1}^n x_i + \sum\limits_{i=1}^n x_i^2\)

其中 \(\sum\limits_{i=1}^n x_i\) 显然还是全图中所有数的和
所以我们就求 \(\sum\limits_{i=1}^n x_i^2\) 的最大值
考虑区间 \(DP\)\(f[x_1][y_1][x_2][y_2][k]\) 表示左上角为 \((x_1, y_1)\) 右下角为 \((x_2, y_2)\) 大小的矩形被切了 \(k\) 刀的 \(\sum\limits_{i=1}^n x_i^2\)
然后枚举切的那刀的位置即可

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 20;
int a[10][10];
int f[10][10][10][10][N];
int n;
double avr;

int count(int x1, int y1, int x2, int y2) {
	int sum = 0;
	for (int i = x1; i <= x2; ++i) {
		for (int j = y1; j <= y2; ++j) sum += a[i][j];
	}
	return sum * sum;
}

void getavr(void) {
	int sum = 0;
	for (int i = 1; i <= 8; ++i) {
		for (int j = 1; j <= 8; ++j) sum += a[i][j];
	}
	avr = (double)sum / (n + 1);
}

int main() {
	scanf("%d", &n);
	--n;
	for (int i = 1; i <= 8; ++i) {
		for (int j = 1; j <= 8; ++j) scanf("%d", &a[i][j]);
	}
	getavr();
	memset(f, 0x3f, sizeof f );
	for (int lx = 1; lx <= 8; ++lx) {
		for (int x1 = 1; x1 + lx - 1 <= 8; ++x1) {
			int x2 = x1 + lx - 1;
			for (int ly = 1; ly <= 8; ++ly) {
				for (int y1 = 1; y1 + ly - 1 <= 8; ++y1) {
					int y2 = y1 + ly - 1;
					f[x1][y1][x2][y2][0] = count(x1, y1, x2, y2);
				}
			}
		}
	}
	
	for (int i = 1; i <= n; ++i) {
		for (int lx = 1; lx <= 8; ++lx) {
			for (int x1 = 1; x1 + lx - 1 <= 8; ++x1) {
				int x2 = x1 + lx - 1;
				for (int ly = 1; ly <= 8; ++ly) {
					for (int y1 = 1; y1 + ly - 1 <= 8; ++y1) {
					int y2 = y1 + ly - 1;
					for (int kx = x1; kx < x2; ++kx) {
						f[x1][y1][x2][y2][i] = min(	f[x1][y1][x2][y2][i], f[x1][y1][kx][y2][0] + f[kx + 1][y1][x2][y2][i - 1]);
						f[x1][y1][x2][y2][i] = min(	f[x1][y1][x2][y2][i], f[x1][y1][kx][y2][i - 1] + f[kx + 1][y1][x2][y2][0]);
					}
					for (int ky = y1; ky < y2; ++ky) {
						f[x1][y1][x2][y2][i] = min(f[x1][y1][x2][y2][i], f[x1][y1][x2][ky][0] + f[x1][ky + 1][x2][y2][i - 1]);
						f[x1][y1][x2][y2][i] = min(f[x1][y1][x2][y2][i], f[x1][y1][x2][ky][i - 1] + f[x1][ky + 1][x2][y2][0]);
					}
				}
				}
			}
		}
	}
	
	double ans = (double)f[1][1][8][8][n];
	ans = ans / (n + 1);
	printf("%.3lf",sqrt(ans - avr * avr));
	
	return 0;
}

E.删数问题

image
image

\(f_{l, r}\) 表示把 \(\left[l, r\right]\) 这段区间的数全删掉的最大收益
那我们就有两种选择:

  1. 一次操作把整个区间都删掉
  2. 枚举断点 \(k\) 拆成 \(f_{l, k} + f_{k + 1, r}\)

然后直接转移即可

  • 为什么这样操作一定是合法的

考虑最后构成答案的操作一定为 \(\left[x_1, x_2\right], \left[x_2, x_3\right], ..., \left[x_{s - 1}, x_s\right]\) 这若干个连续区间

那么我们按顺序删 一定可以保证删除操作合法

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;

namespace steven24 {
	
const int N = 105;	
int a[N];
ll f[N][N];
int n;

ll calc(int l, int r) {
	if (l == r) return 1ll * a[l];
	else return 1ll * abs(a[l] - a[r]) * (r - l + 1);
}

void main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	for (int i = 1; i <= n; ++i) f[i][i] = a[i];
	for (int len = 2; len <= n; ++len) {
		for (int l = 1; l + len - 1 <= n; ++l) {
			int r = l + len - 1;
			f[l][r] = calc(l, r);
			for (int k = l; k < r; ++k) f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r]);
		}
	}
	printf("%lld\n", f[1][n]);
}
	
}

int main() {
	steven24::main();
	return 0;
} 
/*
6
54 29 196 21 133 118
*/

F.恐狼后卫

image
image

有个比较直觉的想法:如果要对一只狼开刀 最好的选择一定是直接砍到死

考虑证明

首先我们要对一只狼砍的刀数是一定的 也就是说我们承受伤害的次数是一定的
并且该狼的攻击力一定 所以有影响的一定是它左右两只狼

假设我承受 \(b_{x_1}, b_{y_1}\) 的攻击力砍了若干刀 \(b_{x_2}, b_{y_2}\) 的攻击力砍了若干刀...等等直到把它砍死
那么也就说明我可以通过一些方式使任何 \((x_i, y_i)\) 都取的到
那么我们选取其中代价最小的一组 把这只狼直接从满血砍到死 一定是小于等于上述操作的代价的

然后考虑如何区间 DP
\(f_{l, r}\) 表示把 \(\left[l + 1, r - 1\right]\) 的狼全砍死需要的最小代价
转移的时候枚举断点 \(k\)\(k\) 从满血砍到死
那么我们就要先把 \(\left[l + 1, k - 1\right]\)\(\left[k + 1, r - 1\right]\) 这两个区间的狼全砍死
然后承受 \(b_l, b_r\) 的代价

点击查看代码
#include <bits/stdc++.h>
#define ll long long

using namespace std;

const int N = 520;
const ll inf = 0x7ffffffffffffff;
ll f[N][N];
int a[N], b[N], hp[N];
int n, atk;

int get(int x) {
    if (x < atk) return 1;
    int ret = x / atk;
    if (x % atk != 0) ++ret;
    return ret;
}

int main() {
    scanf("%d%d", &n, &atk);
    for (int i = 1; i <= n; ++i) scanf("%d%d%d", &a[i], &b[i], &hp[i]);

    for (int len = 3; len <= n + 2; ++len) {
        for (int l = 0; l + len - 1 <= n + 1; ++l) {
            int r = l + len - 1;
            f[l][r] = inf;
            for (int k = l + 1; k <= r - 1; ++k) f[l][r] = min(f[l][r], f[l][k] + get(hp[k]) * (a[k] + b[l] + b[r]) + f[k][r]);
        }
    }

    printf("%lld",f[0][n + 1]);

    return 0;
}

G.矩阵取数

image
image

观察发现每一行之间相互不影响 所以我们可以每一行都分别做一次 然后累加答案

\(f_{l, r}\) 表示 \(\left[l, r\right]\) 这段区间的得分 那么显然有 \(f_{l, r} = \max(f_{l + 1, r} + cost_l, f_{l, r - 1} + cost_r)\)

那么问题的关键就是求出当前拿走这个数的得分

进一步观察可以发现 因为是 \(m\) 次取完 所以如果区间长度已知那么当前操作是第几次取数是一定的

稍微推一下:
总长为 \(m\)
区间长为 \(m\) :第一次取数
区间长为 \(m - 1\) :第二次取数
区间长为 \(1\) :第 \(m\) 次取数
区间长为 \(len\) :第 \(m - len + 1\) 次取数

答案要开高精 偷懒直接 __int128 也行

点击查看代码
#include <bits/stdc++.h>
#define int __int128
using namespace std;

namespace steven24 {
	
int f[101][101];	
int base[101];
int a[101][101];
int n, m;
int ans;
	
inline int read() {
    int xr = 0, F = 1; 
	char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9') 
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

inline void write(int x) {
	if (x < 0) putchar('-'), x = -x;
	if (x >= 10) write(x / 10);
	putchar(x % 10 + '0');
}

void init() {
	base[0] = 1;
	for (int i = 1; i <= m; ++i) base[i] = base[i - 1] * 2;
}

void main() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j) a[i][j] = read();
	}
	init();
	
	for (int s = 1; s <= n; ++s) {
		for (int i = 1; i <= m; ++i) f[i][i] = base[m] * a[s][i];
		for (int len = 2; len <= m; ++len) {
			for (int l = 1; l + len - 1 <= m; ++l) {
				int r = l + len - 1;
				f[l][r] = max(f[l + 1][r] + a[s][l] * base[m - len + 1], f[l][r - 1] + a[s][r] * base[m - len + 1]);
//				write(l), putchar(' '), write(r), putchar(' '), write(f[l][r]), putchar('\n');
			}
		}
		ans += f[1][m];
	}
	write(ans);
}
	
}

signed main() {
	steven24::main();
	return 0;
}
/*
2 3
1 2 3
3 4 2
*/

H.生日欢唱

image
image

其实感觉不太像区间 DP

\(f_{l,r}\) 表示强制使男生第 \(l\) 个和女生第 \(r\) 个配对的最大价值
枚举 \(l, r\) 前面的 \(i, j\) 则有 \(f_{l, r} = f_{i, j} - cost_{i + 1 , l - 1} - cost_{j + 1, r - 1} + val_{l, r}\)
其中 \(cost_{l, r}\) 表示把 \(\left[l, r\right]\) 这段区间的学生全删掉的代价

但是这样转移是 \(\text{O}(n ^ 4)\)

通过进一步思考性质 我们可以发现 如果两个前面都丢掉了人 一定是亏的
所以直接枚举 \(l\) 这边丢掉多少人和 \(r\) 这边丢掉多少人即可
查询代价需要加个前缀和预处理
这样就是 \(\text{O}(n ^ 3)\)

对于答案 我们直接在每个 \(f_{l, r}\) 转移出来之后 算出把剩下学生都丢掉的代价减掉 然后与答案取 \(\max\) 即可

注意初值问题

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;

namespace steven24 {
	
const int N = 521;
ll f[N][N];
int a[2][N];
int d[2][N];
int n;
ll ans;

void init() {
	for (int j = 0; j <= 1; ++j) {
		for (int i = 1; i <= n; ++i) d[j][i] = d[j][i - 1] + a[j][i];
	}
}

ll calc(int l, int r, int opt) {
	if (l > r) return 0;
	int sum = d[opt][r] - d[opt][l - 1];
	return 1ll * sum * sum;
}

void main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[0][i]);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[1][i]);
	init();
	
	memset(f, -0x3f, sizeof f);
	f[0][0] = 0;
	
	for (int l = 1; l <= n; ++l) {
		for (int r = 1; r <= n; ++r) {
			for (int k = 0; k < l; ++k) f[l][r] = max(f[l][r], f[k][r - 1] - calc(k + 1, l - 1, 0) + 1ll * a[0][l] * a[1][r]);
 			for (int k = 0; k < r; ++k) f[l][r] = max(f[l][r], f[l - 1][k] - calc(k + 1, r - 1, 1) + 1ll * a[0][l] * a[1][r]);
			ans = max(ans, f[l][r] - calc(l + 1, n, 0) - calc(r + 1, n, 1));
		}
	}
	
	printf("%lld\n", ans);
}
	
}

int main() {
	steven24::main();
	return 0;
}
/*
3
1 1 5
5 1 1
*/

I.最小代价

image
image

\(DAY^{-1}\)

\(dp_{l, r}\) 表示把 \(\left[l, r\right]\) 这段区间都消掉的代价
\(f_{l, r, x, y}\) 表示把 \(\left[l, r\right]\) 这段区间内除了值域在 \(\left[x, y\right]\) 的所有数都消掉的代价
那么显然有 \(dp_{l, r} = \min(f_{l, r, x, y} + A + B \times (y - x)^2)\)

然后我们考虑 \(f\) 数组的转移
考虑枚举断点 有
\(f_{l, r, x, y} = \min(dp_{l, k} + f_{k + 1, r, x, y})\)
\(f_{l, r, x, y} = \min(f_{l, k, x, y} + dp_{k + 1, r})\)
\(f_{l, r, x, y} = \min(f_{l, k, x, y} + f_{k + 1, r, x, y})\)

对于初值 有
\(dp_{i, i} = A\)
\(f_{i, i, 1 \text{~} a_i, a_i \text{~} maxn} = 0\)

结合上面的定义应该不难理解

此题需要离散化

点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;

namespace steven24 {
	
const int N = 52;
ll f[N][N][N][N], dp[N][N];
int a[N], b[N];
int n, A, B, tot;
ll ans = 0x7fffffff;

void discretization() {
	memcpy(b, a, sizeof a);
	sort(b + 1, b + 1 + n);
	tot = unique(b + 1, b + 1 + n) - b - 1;
	for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + 1 + tot, a[i]) - b;
} 

void dfs(int l, int r) {
	if (l > r) return;
	if (dp[l][r] != 0x3f3f3f3f3f3f3f3f) return;
	for (int x = 1; x <= tot; ++x) {
		for (int y = x; y <= tot; ++y) {
			for (int k = l; k < r; ++k) {
				dfs(l, k);
				dfs(k + 1, r);
				f[l][r][x][y] = min(f[l][r][x][y], dp[l][k] + f[k + 1][r][x][y]);
				f[l][r][x][y] = min(f[l][r][x][y], f[l][k][x][y] + dp[k + 1][r]);
				f[l][r][x][y] = min(f[l][r][x][y], f[l][k][x][y] + f[k + 1][r][x][y]);
				dp[l][r] = min(dp[l][r], f[l][r][x][y] + A + B * (b[y] - b[x]) * (b[y] - b[x]));
			}
		}
	}
}

void main() {
	scanf("%d%d%d", &n, &A, &B);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	discretization(); 
	
	memset(dp, 0x3f, sizeof dp);
	memset(f, 0x3f, sizeof f); 
	for (int i = 1; i <= n; ++i) dp[i][i] = A;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= a[i]; ++j) {
			for (int k = a[i]; k <= tot; ++k) f[i][i][j][k] = 0;
		}
	}
	
	dfs(1, n);
	
	printf("%lld\n", dp[1][n]);
}	
	
}

int main() {
	steven24::main();
	return 0;
} 
/*
10
3 1
7 10 9 10 6 7 10 7 1 2
*/
posted @ 2023-07-01 11:21  Steven24  阅读(53)  评论(0编辑  收藏  举报