题目
思路
1.本题可以说是有难度了,首先思路就不好想到,DP有两个状态,两个状态分别有自己的状态转移方程。其次代码实现起来,细节很多,自己尝试写了一下,debug了一下午也没搞出来,只能搬来LRJ的代码了。
2.回顾一下本题的思考过程:
首先,可以想到LCS,因为每次都是从两个序列种的一个拿元素。最基本的状态就想到了,d(i,j),表示从两个序列分别拿走i,j个元素后最小的指标函数值。问题就出在指标函数的上,本题的指标函数应当是L(c)之和,比较复杂。
容易想到的方式是,让状态变成多维,记录每个元素开始的地方,从而拿进元素时算出L(c)之和。但这样的话时间和空间都无法承受。
正确的方法是,再来一个状态c(i,j),表示已经开始但没有结束的颜色数量,从而可以根据上一个d(i2,j2)来一步算出这次的d(i1,j1),这样的话就引入了状态c(i,j)作为辅助计算的状态,同时这个状态c(i,j)也需要转移计算。
3.状态定义:
- d(i,j):从两个序列拿走i,j组成的子序列,最小的L(c)之和。
- c(i,j):从两个序列拿走i,j组成的子序列,有多少颜色已经开始但没有结束。
4.初状态:都是0
5.答案:d(n,m)
6.状态转移方程:
7.滚动数组:
滚动数组是两维的,为什么不能设置成一维呢?
因为滚动数组的原理是,利用某个旧元素创造一个新元素,再用这个新元素将某个旧元素覆盖。
在本题中,i-1的转移是常见的转移,只有当j逆序枚举时才能正常工作。
而j-1的转移也在同时发生,只有当j正序枚举时才能正常工作。
所以这两个转移,用滚动数组时冲突了,所以我们设置成两维,一维保存i-1的数据,一维保存新数据,并在新数据的一维正序枚举。
根据本人的验证,并不需要将滚动数组二维化就能AC。原因是j-1要求正序枚举,而i-1实际对枚举顺序无要求,所以正序枚举即可。
并且LRJ使用了“^”,简化了很多代码,这里要学习这个“^”的使用:
^,按位异或运算符。
参与运算的两个值,如果两个相应位相同,则结果为0,否则为1。即:0^0=0, 1^0=1, 0^1=1, 1^1=0
x^1的作用:当x为0时返回1,当x为1时返回0。有点类似于!,但只会返回0和1。
代码
LRJ原版
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define _for(i,a,b) for(int i = (a); i<(b); i++)
#define _rep(i,a,b) for(int i = (a); i<=(b); i++)
using namespace std;
const int INF = 100000000;
const int maxn = 5000 + 5;
char p[maxn], q[maxn]; // 从位置1开始
int sp[26], ep[26], sq[26], eq[26]; // sq:start position of q
int d[2][maxn], c[2][maxn]; // 使用滚动数组,注意这里的滚动数有两层
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s%s", p + 1, q + 1);
int n = strlen(p + 1);
int m = strlen(q + 1);
_rep(i, 1, n) p[i] -= 'A';
_rep(i, 1, m) q[i] -= 'A';
// 计算s和e
_for(i, 0, 26) { sp[i] = sq[i] = INF; ep[i] = eq[i] = 0; } // 此处的初始化很关键
_rep(i, 1, n) {
sp[p[i]] = min(sp[p[i]], i);
ep[p[i]] = i;
}
_rep(i, 1, m) {
sq[q[i]] = min(sq[q[i]], i);
eq[q[i]] = i;
}
// dp
int t = 0;
memset(c, 0, sizeof(c));
memset(d, 0, sizeof(d));
_rep(i, 0, n) {
_rep(j, 0, m) {
if (i == 0 && j == 0) continue;
// 计算d
int v1 = INF, v2 = INF;
if (i) v1 = d[t ^ 1][j] + c[t ^ 1][j];
if (j) v2 = d[t][j - 1] + c[t][j - 1];
d[t][j] = min(v1, v2);
// 计算c
if (i) {
c[t][j] = c[t ^ 1][j];
if (sp[p[i]] == i && sq[p[i]] > j) c[t][j]++;
if (ep[p[i]] == i && eq[p[i]] <= j) c[t][j]--;
}
else if (j) {
c[t][j] = c[t][j - 1];
if (sq[q[j]] == j && sp[q[j]] > i) c[t][j]++;
if (eq[q[j]] == j && ep[q[j]] <= i) c[t][j]--;
}
}
t ^= 1;
}
printf("%d\n", d[t ^ 1][m]);
}
return 0;
}
滚动数组去二维
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define _for(i,a,b) for(int i = (a); i<(b); i++)
#define _rep(i,a,b) for(int i = (a); i<=(b); i++)
using namespace std;
const int INF = 100000000;
const int maxn = 5000 + 5;
char p[maxn], q[maxn]; // 从位置1开始
int sp[26], ep[26], sq[26], eq[26]; // sq:start position of q
int d[maxn], c[maxn]; // 使用滚动数组,注意这里的滚动数有两层
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s%s", p + 1, q + 1);
int n = strlen(p + 1);
int m = strlen(q + 1);
_rep(i, 1, n) p[i] -= 'A';
_rep(i, 1, m) q[i] -= 'A';
// 计算s和e
_for(i, 0, 26) { sp[i] = sq[i] = INF; ep[i] = eq[i] = 0; } // 此处的初始化很关键
_rep(i, 1, n) {
sp[p[i]] = min(sp[p[i]], i);
ep[p[i]] = i;
}
_rep(i, 1, m) {
sq[q[i]] = min(sq[q[i]], i);
eq[q[i]] = i;
}
// dp
memset(c, 0, sizeof(c));
memset(d, 0, sizeof(d));
_rep(i, 0, n) {
_rep(j, 0, m) {
if (i == 0 && j == 0) continue;
// 计算d
int v1 = INF, v2 = INF;
if (i) v1 = d[j] + c[j];
if (j) v2 = d[j - 1] + c[j - 1];
d[j] = min(v1, v2);
// 计算c
if (i) {
c[j] = c[j];
if (sp[p[i]] == i && sq[p[i]] > j) c[j]++;
if (ep[p[i]] == i && eq[p[i]] <= j) c[j]--;
}
else if (j) {
c[j] = c[j - 1];
if (sq[q[j]] == j && sp[q[j]] > i) c[j]++;
if (eq[q[j]] == j && ep[q[j]] <= i) c[j]--;
}
}
}
printf("%d\n", d[m]);
}
return 0;
}