「REMAKE系列」线性dp篇
常见模型、技巧总结
LIS、LCS模型
LIS
- 结论题 \(I\) [HAOI2006]数字序列
LCS
有趣线性dp
- 定义状态为修改中。 CF1155D
习题
洛谷——「能力综合提升题单-线性DP篇」
P2501 [HAOI2006]数字序列
现在我们有一个长度为 \(n\) 的整数序列 \(a\)。但是它太不好看了,于是我们希望把它变成一个单调严格上升的序列。但是不希望改变过多的数,也不希望改变的幅度太大。
第一行输出一个整数,表示最少需要改变多少个数。
第二行输出一个整数,表示在改变的数最少的情况下,每个数改变的绝对值之和的最小值。
- 对于 \(100\%\) 的数据,保证 \(1 \leq n \leq 3.5 \times 10^4\),\(1 \leq a_i \leq 10^5\)。数据保证 \(a_i\) 随机生成。
思路
- 结论题两个结论,数据随机 \(O(n^2)\) 可以过。
- 对于第一问,构造 \(b[i] = a[i] - i\),求 b[] 的最长单调不减子序列长度即可。
- 对于第二问。考虑 b[] 中最长单调不减子序列中的相邻元素下标为 i, j,对应下标之间的大小一定不在 [b[i], b[j]] 之间,否则子序列可以更长。
- 那么对于每一对相邻元素(序列不止一个),最终的结果一定是中间某一个 \(k(i\leq k \leq j)\), 使得
b[i..k] = b[i], b[k+1...j]=b[j]
,这样的结果是最优的。 - 所以对于每一对相邻元素的区间,采用 dp 转移,设
dp[i]
表示处理以下标 \(i\) 为结尾的最长单调不减子序列最小代价 - 枚举分界点 k。判断相邻元素区间合法,在 LIS 时记录每个下标作为末尾对应的最长长度。
- 那么对于每一对相邻元素(序列不止一个),最终的结果一定是中间某一个 \(k(i\leq k \leq j)\), 使得
const int N = 4e4 + 10;
int a[N], n, b[N], v[N];
ll dp[N];
ll s[N], suf[N], L[N];
vector<int> pos[N];
int main() {
re(n);
for (int i = 1; i <= n; i++)
re(a[i]), b[i] = a[i] - i;
b[0] = -2e9;
b[n + 1] = 2e9; // 最后一个(段)元素也是会被修改的,所以将边界搞到 n + 1
int len = 0;
for (int i = 1; i <= n + 1; i++) {
int l = 0, r = len + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (b[v[mid]] > b[i]) r = mid;
else l = mid + 1;
}
if (l == len + 1) v[++len] = i;
else v[l] = i;
L[i] = l;
pos[l].pb(i);
}
int ans1 = n - len + 1;
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
pos[0].pb(0);
for (int i = 1; i <= n + 1; i++) {
for (auto pre: pos[L[i] - 1]) {
if (pre > i || b[pre] > b[i]) continue;
s[pre] = 0;
suf[i - 1] = 0;
for (int j = pre + 1; j <= i - 1; j++) {
s[j] = s[j - 1] + abs(b[j] - b[pre]);
}
for (int j = i - 2; j >= pre; j--)
suf[j] = suf[j + 1] + abs(b[j + 1] - b[i]);
for (int k = pre; k <= i - 1; k++) { // pre, k --- k + 1, i
dp[i] = min(dp[i], dp[pre] + s[k] + suf[k]);
}
}
}
printf("%lld\n%lld\n", ans1, dp[n + 1]);
return 0;
}
Codeforces
CF1155D. Beautiful Array
定义三个状态,未修改、正在修改、修改结束了,相互转移即可
const int N = 3e5 + 10;
ll f[N][3], s[N];
ll a[N];
int main() {
ll n, x;
re(n), re(x);
ll ans = 0;
for (int i = 1; i <= n; i++) {
re(a[i]);
f[i][0] = max(f[i - 1][0] + a[i], max(a[i], 0ll));
f[i][1] = max(max(f[i - 1][0], f[i - 1][1]) + x * a[i], max(0ll, x * a[i]));
f[i][2] = max(max({f[i - 1][2], f[i - 1][1], f[i - 1][0]}) + a[i], max(0ll, a[i]));
ans = max({ans, f[i][0], f[i][1], f[i][2]});
}
printf("%lld\n", ans);
return 0;
}
CF1446B. Catching Cheaters
- LCS变形,将状态转移代价改为对答案的贡献。
const int N = 5010;
int dp[N][N];
int main() {
int n, m;
IOS;
cin >> n >> m;
string a, b;
cin >> a >> b;
a = " " + a, b = " " + b;
int ans = 0;
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]) - 1;
dp[i][j] = max(0, dp[i][j]);
if (a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 2);
ans = max(ans, dp[i][j]);
}
}
cout << ans << endl;
return 0;
}
CF666A. Reberland Linguistics
已知有长度为\(n\)的由小写字母构成的一个字符串, 现在对字符串进行一些操作
- 选择一个长度\(\color{red}\text{大于}\) \(4\)的“根”字符串
- 然后在“根”字符串之后连接上任意个长度为2或3的“后缀”字符串。
- 唯一的限制条件是,不可以连续接上两个相同的“后缀”字符串。
现在求构造过程中,有可能被用作“后缀”的字符串共有多少种。\(P.S:\)多个同样的“后缀”字符串只算一种。
思路
- \(dp[i][j]\) i 开始接 j 个字符串是合法。
- 从后往前做dp,转移条件是后面相同长度的串 不等且合法 或者 不同长度串合法
int dp[10010][4];
// dp[i][j] i 开始接 j 个字符串是合法。
int main() {
IOS;
string s;
cin >> s;
s = " " + s;
int n = s.size() - 1;
set<string> res;
if (n <= 8) {
if (n <= 6) {
cout << 0 << endl;
return 0;
}
else if (n == 7) {
res.insert(s.substr(6));
}
else {
res.insert(s.substr(6));
res.insert(s.substr(7));
}
cout << SZ(res) << endl;
for (auto t: res)
cout << t << endl;
return 0;
}
dp[n - 1][2] = 1, dp[n - 2][3] = 1;
res.insert(s.substr(n - 1));
res.insert(s.substr(n - 2));
for (int i = n - 3; i >= 6; i--) {
for (int j = 2; j <= 3; j++) {
if (n - (i + j) + 1 < 2) continue;
string a = s.substr(i, j), b = s.substr(i + j, j);
if (dp[i + j][5 - j] || (a != b && dp[i + j][j])) dp[i][j] = 1;
if (dp[i][j]) res.insert(a);
}
}
cout << SZ(res) << endl;
for (auto t: res)
cout << t << endl;
return 0;
}
CF682D. Alyona and Strings
- 设 \(dp[i][j][k]\) 表示为 a 串前 i 位与 b 串前 j 位匹配 k 次的最大长度
- 转移简单,但每次转移完需要更新为前缀最大值
int n, m, K;
const int N = 1010;
int dp[N][N][11];
char a[N], b[N];
int main() {
re(n), re(m), re(K);
scanf("%s", a + 1);
scanf("%s", b + 1);
int ans = 0;
for (int k = 1; k <= K; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) {
dp[i][j][k] = max(dp[i - 1][j - 1][k] + 1, dp[i - 1][j - 1][k - 1] + 1);
}
ans = max(ans, dp[i][j][k]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
dp[i][j][k] = max(dp[i][j][k], max(dp[i - 1][j][k], dp[i][j - 1][k]));
}
}
printf("%d\n", ans);
return 0;
}
CF1183H. Subsequences (hard version)
- 设状态为 \(dp[i][j]\) 前 i 个字符,序列长度为 j 的方案数。
- 转移:\(dp[i][j] = dp[i - 1][j] + (dp[i - 1][j - 1] - dp[pre[i] - 1][j - 1])\)
const int N = 1010;
ll dp[N][N], n, k, pos[26], pre[N];
char s[N];
int main() {
re(n), re(k);
scanf("%s", s + 1);
for (int i = 1; i <= n; i++) {
pre[i] = pos[(s[i] - 'a')];
pos[(s[i] - 'a')] = i;
}
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
dp[i][0] = 1;
for (int j = 1; j <= i; j++) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
if (pre[i])
dp[i][j] -= dp[pre[i] - 1][j - 1];
// printf("dp[%d][%d]=%d\n", i,j,dp[i][j]);
}
}
ll sum = 0;
for (int j = n; j >= 0 && k > 0; j--) {
ll cnt = dp[n][j];
if (k >= cnt) {
k -= cnt;
sum += cnt * 1ll * (n - j);
}
else {
sum += k * 1ll * (n - j);
k = 0;
}
}
if (k) puts("-1");
else
printf("%lld\n", sum);
return 0;
}