跳到主要内容

线性动态规划

编辑距离

题目描述

AABB 是两个字符串。我们要用最少的字符操作次数,将字符串 AA 转换为字符串 BB。这里所说的字符操作共有三种:

  1. 删除一个字符;
  2. 插入一个字符;
  3. 将一个字符改为另一个字符。

A,BA, B 均只包含小写字母。

输入格式

第一行为字符串 AA;第二行为字符串 BB;字符串 A,BA, B 的长度均小于 20002000

输出格式

只有一个正整数,为最少字符操作次数。

样例输入 #1

sfdqxbw
gfdgw

样例输出 #1

4

对于 100%100 \% 的数据,1A,B20001 \le |A|, |B| \le 2000

题目

状态表示:f[i][j]表示将A的1~i变成B的1~j的操作次数的最小值

状态计算:三种操作方式如下:

  • 删除:将A[i]删除后应该与B[1~j]相同,说明A[1~(i-1)]==B[1~j]

    • 方程为:f[i-1][j]+1
  • 插入:在A[i]之后插入后与B[1~j]相同,则说明A[1~i]==B[1~j-1]

    • 方程为:f[i][j-1]+1
  • 替换:将A[i]替换后,A[1~i]==B[1~j]分两种情况:

    • 第一种是A[i]==B[j]已经相等了不需要替换,f[i-1][j-1]
    • 第二种是不想等,需要替换一次,f[i-1][j-1]+1

状态转移方程为上述三个取min

初始化的时候需要将:

  • f[i][0]=i表示删除i个字符与B相等
  • f[0][i]=i表示添加i个字符与B相等

或者这样考虑:

f[i][j]表示将A的1~i变成B的1~j的操作次数的最小值

  • 如果A[i]==B[j],则f[i][j]=f[i-1][j-1],

  • 如果A[i]!=B[j], 使用三种操作,取最小值

    • 修改:将a[i]改为b[i], f[i-1][j-1]+1
    • 插入:在a[i]后面插入b[i],f[i][j-1]+1
    • 删除:将a[i]删除,f[i-1][j]+1

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;
const int N = 2010;
int f[N][N];//将A 1-i变成 B 1-j的最小操作次数
string a, b;


signed main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
cin >> a >> b;
int n = a.size(), m = b.size();
a = " " + a, b = " " + b;
for (int i = 1; i <= n; i++) f[i][0] = i;
for (int i = 1; i <= m; i++) f[0][i] = i;

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1];
else f[i][j]= min(f[i-1][j-1], min(f[i-1][j],f[i][j-1]))+1;
}
}
cout << f[n][m];

return 0;
}

899. 编辑距离 - AcWing

给定 nn 个长度不超过 10$$10 的字符串以及 mm次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的 nn 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式

第一行包含两个整数 nnmm

接下来 nn 行,每行包含一个字符串,表示给定的字符串。

再接下来 mm 行,每行包含一个字符串和一个整数,表示一次询问。

字符串中只包含小写字母,且长度均不超过 10$$10

输出格式

输出共 mm 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。

数据范围

1 \le n, m \le 1000$$1 \le n, m \le 1000,

输入样例:

3 2
abc
acd
bcd
ab 1
acbd 2

输出样例:

1
3

题目

和上一题一样,将dp封装为一个函数即可。暴力统计是否能够转换。

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;
const int N = 20;
int f[N][N];

int edit(string a, string b) {
int n = a.size(), m = b.size();
a = " " + a, b = " " + b;
for (int i = 1; i <= n; i++) f[i][0] = i;
for (int i = 1; i <= m; i++) f[0][i] = i;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1];
else f[i][j] = min(f[i - 1][j - 1], min(f[i][j - 1], f[i - 1][j])) + 1;

}
}
return f[n][m];
}


signed main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
int n, m;
cin >> n >> m;
vector<string> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
while (m--) {
string s;
int limit;
cin >> s >> limit;
int res = 0;
for (int i = 1; i <= n; i++) {
if (edit(a[i], s) <= limit) res++;
}
cout << res << endl;
}

return 0;
}

[NOIP1999 普及组] 导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

一行,若干个整数,中间由空格隔开。

输出格式

两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

样例输入 #1

389 207 155 300 299 170 158 65

样例输出 #1

6
2

对于前 50%50\% 数据(NOIP 原题数据),满足导弹的个数不超过 10410^4 个。该部分数据总分共 100100 分。可使用O(n2)\mathcal O(n^2) 做法通过。
对于后 50%50\% 的数据,满足导弹的个数不超过 10510^5 个。该部分数据总分也为 100100 分。请使用 O(nlogn)\mathcal O(n\log n) 做法通过。

对于全部数据,满足导弹的高度为正整数,且不超过 5×1045\times 10^4

题目 (LIS+贪心 O(n2)O(n^2))

第一问是LIS问题:

LIS 是 Longest ascending subsequence 的缩写

显然每套导弹拦截系统拦截导弹高度为步上升子序列,也就是最长不上升子序列,只需要求最长不上升子序列的长度就可以了,

状态的计算:

for (int j = 0; j < i; j++) 
if (q[j] >= q[i]) { // 不高于, 可以等于, 最长下降子序列(不完全下降,可以等于),严格来说是不上升子序列
f[i] = max(f[i], f[j] + 1);

第二问是贪心问题:

贪心思路:

  • 较大的数的末尾元素比较小的数作为末尾元素的子序列更好,因为这样可以让这个子序列变得更长,如果放了一个较小的数,可能放的数就没有那么长了

贪心步骤:

  1. 开一个数组q[]记录所有下降子序列末尾元素

  2. 遍历每一个数,对于当前的这个数:

    1. q[]中所有的数都比这个数大,那么我们就扩大q[]的长度,并且把这个数放到q[]中刚开的地方
    2. q[]中存在比这个数小的数,那么我们就把当前的这个数覆盖到找到的比这个数小的最大的数
  3. 每次将一个较小的数换成一个较大的数作为末尾元素存储到q[], 随着q[]的长度扩大, 也就表示此时新增x比所有末尾元素都大时; q[]随下标增大元素值也越大, 因此q[]是单调上升的数组

这题的思路和AcWing 896. 最长上升子序列 II 一样,我们可以得出结论:

求解至少需要多少个下降子序列的数目

求解至多含有多少个上升子序列的数目是一个对偶问题

注:数组q[]是单调不减的!!!

代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n;
int q[N];
int f[N], g[N];

int main() {
while (cin >> q[n]) n++;
int res = 0;
for (int i = 0; i < n; ++i) {
f[i] = 1;
for (int j = 0; j < i; j++) {
if (q[j] >= q[i]) { // 不高于, 可以等于, 最长下降子序列
f[i] = max(f[i], f[j] + 1);
}
}
res = max(res, f[i]);
}
cout << res << endl;
int cnt = 0;//当前子序列的个数
for (int i = 0; i < n; i++) {
int k = 0;//记录下从前往后找到的比当前的数q[i]小的最大的数
while (k < cnt && g[k] < q[i]) k++;// 当前的序列结尾的数<=我们的数
g[k] = q[i];
if (k >= cnt) cnt++;//没有任何一个数是大于等于我们当前数的
}
cout << cnt << endl;
return 0;
}

思路2(O(n2)O(n^2))

可以把题目理解为:

  • 给定一个序列,求至少包含多少个下降子序列数目

等价于

  • 给定一个序列,求至多包含多少个上升子序列数目

也就是将题目转换为求解最长上升子序列的长度。

代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int n;
int a[N], f[N];

int main()
{
// 第一问:问题是求最长下降子序列的长度
while(cin >> a[n])n++;
int res = 0;
for (int i = 0; i < n; ++i) {
f[i] = 1;

for (int j = 0; j < i; ++j)
if (a[i] <= a[j])
f[i] = max(f[i], f[j] + 1);
res = max(res, f[i]);
}

// 第二问:问题转化为求解最长上升子序列的长度
int len = 0;
for (int i = 0; i < n; ++i) {

f[i] = 1;
for (int j = 0; j < i; ++j)
if (a[i] > a[j])
f[i] = max(f[i], f[j] + 1);
len = max(len, f[i]);
}

cout << res << endl;
cout << len << endl;
return 0;
}

思路3(二分+LIS O(nlogn)O(nlogn))

注意这里第一问需要倒着求最长上升子序列,这里的上升可以相等,因此使用的是upper_bound函数

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 1e5 + 10;
int a[N];
int n = 1;

signed main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
while (cin >> a[n])n++;
n--;
vector<int> f;
f.push_back(a[n]);
for (int i = n - 1; i >= 1; i--) {
if (a[i] >= f.back()) f.push_back(a[i]);
else {//找到第一个大于a[i]的数,换成a[i]
*std::upper_bound(f.begin(), f.end(), a[i]) = a[i];
}
}
cout << f.size() << endl;
vector<int> g;
g.push_back(a[1]);
for (int i = 2; i <= n; i++) {
if (a[i] > g.back())g.push_back(a[i]);
else *std::lower_bound(g.begin(), g.end(), a[i]) = a[i];
}
cout << g.size() << endl;

return 0;
}

187. 导弹防御系统

题目链接:https://www.acwing.com/problem/content/189/

为了对抗附近恶意国家的威胁,RR 国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为 33 和高度为 44的两发导弹,那么接下来该系统就只能拦截高度大于 44 的导弹。

给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式

输入包含多组测试用例。

对于每个测试用例,第一行包含整数 nn,表示来袭导弹数量。

第二行包含 nn不同的整数,表示每个导弹的高度。

当输入测试用例 n=0n=0 时,表示输入终止,且该用例无需处理。

输出格式

对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。

数据范围

1n501 \le n \le 50

输入样例:

5
3 5 2 4 1
0

输出样例:

2

对于给出样例,最少需要两套防御系统。

一套击落高度为 3,43,4 的导弹,另一套击落高度为 5,2,15,2,1 的导弹。

题目(DFS,迭代加深,剪枝,贪心)

up[k]down[k]记录第k套上升(下降)系统目前所拦截的最后一个导弹

dfs(u,v,t)意味着已有u个上升,v个下降,正在处理第t个数

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

int n;
const int N = 60;
int a[N];
int up[N], down[N];
int ans = 0;

/**
* @param u 当前第几个导弹
* @param s 上
* @param x 下
*/
void dfs(int u, int s, int x) {
if (s + x >= ans) return;
if (u >= n + 1) {
ans = s + x;
return;
}
//将当前数放到上升子序列中
int k = 0;
while (k < s && up[k] >= a[u]) k++; //大于等于当前数
int t = up[k];
up[k] = a[u];
if (k < s) dfs(u + 1, s, x);
else dfs(u + 1, s + 1, x);
up[k] = t;//回溯

//将当前数放到下降子序列中
k = 0;
while (k < x && down[k] <= a[u]) k++;//小于等于当前数
t = down[k];
down[k] = a[u];
if (k < x) dfs(u + 1, s, x);
else dfs(u + 1, s, x + 1);
down[k] = t;
}

void solve() {
for (int i = 1; i <= n; i++) cin >> a[i];
ans = n;
dfs(1, 0, 0);
cout << ans << endl;
}

signed main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
while (cin >> n, n) {
solve();
}
return 0;
}

AcWing 272. 最长公共上升子序列

题目链接:https://www.acwing.com/problem/content/274/

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列 A 和 B ,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列 A 和 B 的长度均不超过 3000 。

输入格式

第一行包含一个整数 NN,表示数列ABA,B 的长度。

第二行包含 NN 个整数,表示数列 AA

第三行包含 NN 个整数,表示数列 BB

输出格式

输出一个整数,表示最长公共上升子序列的长度。

数据范围

1N30001 \le N \le 3000,序列中的数字均不超过 23112^{31}-1

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

题目

动态规划:

  • 状态表示:f[i][j]代表所有a[1 ~ i]b[1 ~ j]中以b[j]结尾的公共上升子序列的集合的子序列长度的最大值

  • 状态计算:首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:

    • 不包含a[i]的子集,最大值是f[i - 1][j]
    • 包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
      • 子序列只包含b[j]一个数,长度是1;
      • 子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1
      • 子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1

代码

#include <bits/stdc++.h>

#define int long long
using namespace std;

const int N = 3010;
int f[N][N];
int a[N], b[N];
int n;

signed main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i] != b[j]) f[i][j] = f[i - 1][j];
else {
int mx = 0;
for (int k = 1; k < j; k++) {
if (b[k] < b[j]) mx = max(mx, f[i - 1][k]);
}
f[i][j] = mx + 1;
}
}
}
int res = 0;
for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
cout << res << endl;

return 0;
}

优化:使用define int long long 会内存超限

开一个变量maxn记录下子序列的倒数第二个元素在b[]中是哪个数的最大值,防止每次都去求一次最大值。

代码

#include <bits/stdc++.h>


using namespace std;

const int N = 3010;
int f[N][N];
int a[N], b[N];
int n;

int main() {
#ifndef ONLINE_JUDGE
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
#endif
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= n; i++) {
int mx = 0;
for (int j = 1; j <= n; j++) {
if (a[i] != b[j]) f[i][j] = f[i - 1][j];
else f[i][j] = max(f[i][j], mx + 1);
if (a[i] > b[j]) mx = max(mx, f[i - 1][j] );
}
}
int res = 0;
for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
cout << res << endl;

return 0;
}