参考《算法竞赛进阶指南》、AcWing题库

1049. 大盗阿福

题目描述

阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。

这条街上一共有 NN 家店铺,每家店中都有一些现金。

阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。

作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。

他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?

输入格式

输入的第一行是一个整数 TT,表示一共有 TT 组数据。

接下来的每组数据,第一行是一个整数 NN ,表示一共有 NN 家店铺。

第二行是 NN 个被空格分开的正整数,表示每一家店铺中的现金数量。

每家店铺中的现金数量均不超过1000。

输出格式

对于每组数据,输出一行。

该行包含一个整数,表示阿福在不惊动警察的情况下可以得到的现金数量。

数据范围

1T501 \le T \le 50,
1N1051 \le N \le 10^5

输入样例

1
2
3
4
5
2
3
1 8 2
4
10 7 6 14

输出样例

1
2
8
24

样例解释

对于第一组样例,阿福选择第2家店铺行窃,获得的现金数量为8。

对于第二组样例,阿福选择第1和4家店铺行窃,获得的现金数量为10+14=24。

算法分析

在DP的阶段表示中,如果前一个阶段的决策会影响到后面的阶段的决策,那么我们就可以用状态机的形式来做,具体做法就是描述清楚当前的每一个决策

理解题意
  1. 限制
    • 条件限制: 不能连续选择物品
  2. 目标
    • Max物品价值

相比于0101背包,限制条件从物品的体积变为一个条件限制: 不能连续选择物品,也就是我们此时需要关注物品 ii 能否被选。原本限制体积的状态在本题中用于表示是否选择。可以用状态机模型清晰的表示不同状态间的关系.

状态机模型

状态机: 用一些确定的状态及状态间的关系描述一个过程.

本题中需要表示第ii个物品选/不选两种状态, 且不能连续选择. 状态间的关系如图:

2.png

上图具体含义:

  • 状态: 用01分别表示不/选.

  • 有向边: 表示下一个物品选/不选, 每条边表示一次决策; 边权表示这次决策的收益.

  • 阿福的一种选择与图中长度为NN的一条路径一一对应.

依照状态机模型我们就很容易做DPDP分析了.

DPDP分析

状态表示 dp(i,0)dp(i, 0)/dp(i,1)dp(i, 1)

  • 集合: dp(i,j)dp(i, j)表示所有走了ii步且最终状态为jj的路径. j[0,1]j\in [0, 1].

  • 属性: Max

状态计算

依据最后一步的选择, 在状态机模型中对应当前状态从何而来:

  • dp(i,0)dp(i, 0): 其上一个状态可以是dp(i1,0)dp(i - 1, 0)/dp(i1,1)dp(i - 1, 1), 且对应边的权重均为0 ,有dp(i,0)=max{dp(i1,0),dp(i1,1)}dp(i, 0) = max\lbrace dp(i - 1, 0), dp(i - 1, 1)\rbrace.

  • dp(i,1)dp(i, 1): 其上一个状态只能是dp(i1,0)dp(i - 1, 0). 且对应边的权重为wiw_i, 即物品ii的价值,有p(i,1)=dp(i1,0)+wip(i, 1) = dp(i - 1, 0) + w_i.

因为阿福的每个决策都与图中长度为NN的路径一一对应, 所以DPDP的计算过程相当于将所有路径的可能都计算了一遍.

Solution

状态机动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
int money[N], f[N][2];
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> money[i];
f[0][0] = 0, f[0][1] = -INF;
for (int i = 1; i <= n; ++i) {
f[i][0] = max(f[i - 1][0], f[i - 1][1]);
f[i][1] = f[i - 1][0] + money[i];
}
cout << max(f[n][0], f[n][1]) << endl;
}
}

线性动态规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int money[N], f[N];
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> money[i];
f[0] = 0, f[1] = money[1];
for (int i = 2; i <= n; ++i) {
f[i] = max(f[i - 1], f[i - 2] + money[i]);
}
cout << f[n] << endl;
}
}

1057. 股票买卖

题目描述

给定一个长度为 NN 的数组,数组中的第 ii 个数字表示一个给定股票在第 ii 天的价格。

设计一个算法来计算你所能获取的最大利润,你最多可以完成 kk 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。

输入格式

第一行包含整数 NNkk,表示数组的长度以及你可以完成的最大交易数量。

第二行包含 NN 个不超过 1000010000 的正整数,表示完整的数组。

输出格式

输出一个整数,表示最大利润。

数据范围

1N1051 \le N \le 10^5,
1k1001 \le k \le 100

输入样例1:

1
2
3 2
2 4 1

输出样例1:

1
2 

输入样例2:

1
2
6 2
3 2 6 5 0 3

输出样例2:

1
7 

样例解释

样例1:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

样例2:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。共计利润 4+3 = 7.

算法分析

cb8092d74acad480814865d74231b98.png

初始化:

  • f[0][j][0/1] = -INF,无效,0个物品却进行了j次交易,不可能
  • f[i][0][1] = -INF,无效,没进行交易却持有物品,不可能
  • f[i][0][0] = 0,有效,没进行交易,也未持有,收益0

先将f全部设置为无效状态-INF,再单独处理有效的起始状态f[i][0][0] = 0

入口为f[0][0][0]

注意:

  • 这里状态机的过程,对于每个股票,要么就买,要么就卖,不能说是买了然后在同一个点直接卖掉,这样是不符合状态机模型的,因此对于上述转移方程可以会有人提出疑问。

  • 为什么状态转移方程不能是下面代码,即卖的时候才算做了一次交易,原代码是买的时候才算一次交易

1
2
f[i][j][0] = max(f[i - 1][j - 1][1] + w[i], f[i - 1][j][0]);
f[i][j][1] = max(f[i - 1][j][0] - w[i], f[i - 1][j][1]);

终究要回归到状态转移的起点,第一支股票只有买,和不买这两个操作,一定不可能是卖和不卖的这两个操作,因此第一支股票如果买入时,必须按照一次交易处理。否则如果第一次股票如果买入时,不按一次交易处理,也就代表着第一支股票卖出才算一次交易,也就代表着在第一支股票卖出之前还买了一支股票,显然是矛盾的。

时间复杂度 O(nm)O(nm)

空间复杂度 O(m)O(m)

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 110;
int f[N][M][2];
int money[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> money[i];

//f[0][j][0/1] = 无效,0个物品却进行了j次交易,不可能
//f[i][0][1] = 无效,没进行交易却持有物品,不可能
//f[i][0][0] = 0,没进行交易,也未持有,有效状态,收益0
memset(f, 0xc0, sizeof f);
for (int i = 0; i <= n; ++i) f[i][0][0] = 0;

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {//j = 0已在初始化时计算,f[i][0][1] = 无效,f[i][0][0] = 0
f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + money[i]);
f[i][j][1] = max(f[i - 1][j - 1][0] - money[i], f[i - 1][j][1]);
}
}

int ans = 0;
for (int j = 0; j <= m; ++j) ans = max(ans, f[n][j][0]);
cout << ans;
}

滚动数组优化空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 110;
int f[2][M][2];
int money[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> money[i];

memset(f, 0xc0, sizeof f);
f[0][0][0] = f[1][0][0] = 0;

for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
f[i & 1][j][0] = max(f[(i - 1) & 1][j][0], f[(i - 1) & 1][j][1] + money[i]);
f[i & 1][j][1] = max(f[(i - 1) & 1][j - 1][0] - money[i], f[(i - 1) & 1][j][1]);
}
}

int ans = 0;
for (int j = 0; j <= m; ++j) ans = max(ans, f[0][j][0]);
for (int j = 0; j <= m; ++j) ans = max(ans, f[1][j][0]);
cout << ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 110;
int f[M][2];
int money[N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> money[i];

memset(f, 0xc0, sizeof f);
f[0][0] = 0;

for (int i = 1; i <= n; ++i) {
for (int j = m; j >= 1; --j) {
f[j][0] = max(f[j][0], f[j][1] + money[i]);
f[j][1] = max(f[j - 1][0] - money[i], f[j][1]);
}
}

int ans = 0;
for (int j = 0; j <= m; ++j) ans = max(ans, f[j][0]);
cout << ans;
}

1058. 股票买卖 V

题目描述

给定一个长度为 NN 的数组,数组中的第 ii 个数字表示一个给定股票在第 ii 天的价格。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 11 天)。

输入格式

第一行包含整数 NN,表示数组长度。

第二行包含 NN 个不超过 1000010000 的正整数,表示完整的数组。

输出格式

输出一个整数,表示最大利润。

数据范围

1N1051 \le N \le 10^5

输入样例

1
2
5
1 2 3 0 2

输出样例

1
3 

样例解释

对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。

算法分析

6bc433e74040b6b70db97b7da6b89e4.png

初始化:f[0][2] = 0f[0][0] = f[0][1] = -INF, 因为在第0个股票一定是无货的,且可以买入,必定从2这个状态开始转移才有效

出口:可能最后一天才卖出,也可能最后一天是最后一次卖出操作好几天后了,故取max(f[n][1],f[n][2])

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int money[N], f[N][3];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> money[i];

memset(f, 0xc0, sizeof f);
f[0][2] = 0;

for (int i = 1; i <= n; ++i) {
f[i][0] = max(f[i - 1][0], f[i - 1][2] - money[i]);
f[i][1] = f[i - 1][0] + money[i];
f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}

cout << max(f[n][1], f[n][2]);
}

滚动数组优化空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int money[N], f[2][3];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> money[i];

memset(f, 0xc0, sizeof f);
f[0][2] = 0;

for (int i = 1; i <= n; ++i) {
f[i & 1][0] = max(f[(i - 1) & 1][0], f[(i - 1) & 1][2] - money[i]);
f[i & 1][1] = f[(i - 1) & 1][0] + money[i];
f[i & 1][2] = max(f[(i - 1) & 1][1], f[(i - 1) & 1][2]);
}

cout << max(f[n & 1][1], f[n & 1][2]);//注意这里的 n & 1
}

1052. 设计密码(字符串匹配自动机 + KMP)

题目描述

你现在需要设计一个密码 SSSS 需要满足:

  • SS 的长度是 NN
  • SS 只包含小写英文字母;
  • SS 不包含子串 TT

例如:abcabcabcdeabcdeabcdeabcde 的子串,abdabd 不是 abcdeabcde 的子串。

请问共有多少种不同的密码满足要求?

由于答案会非常大,请输出答案模 109+710^9+7 的余数。

输入格式

第一行输入整数N,表示密码的长度。

第二行输入字符串T,T中只包含小写字母。

输出格式

输出一个正整数,表示总方案数模 109+710^9+7 后的结果。

数据范围

1N501 \le N \le 50,
1TN1 \le |T| \le NT|T|TT的长度。

输入样例1:

1
2
2
a

输出样例1:

1
625 

输入样例2:

1
2
4
cbc

输出样例2:

1
456924 

算法分析

字符串匹配自动机 + KMP详细解释见 一文吃透字符串匹配!

f[i][j]:生成了i个密码,且状态停留在j,即有限自动机读入了i个字符,且ϕ(Ti)=j\phi(T_i)=j

状态j也表示已成功匹配j个字符

算法流程:

  • 循环生成密码字符个数,正在生成第i个字符
  • 枚举生成i - 1个密码字符后,所停留的状态 ϕ(Ti1)=j\phi(T_{i-1})=j
  • 枚举正在生成的是什么字符,T[i]=azT[i] = a \sim z
  • 计算状态转移,求 ϕ(Ti)=δ(j,T[i])\phi(T_i) = \delta(j,T[i]) ,转移函数用KMP算法的前缀函数 π()\pi() 来实现
  • 状态转移从jq,更新计算f[i][q]的方案数
  • 计算最终所有方案数,生成完n个密码字符,且状态不停留在m的方案都满足条件,累加起来

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;
const int N = 60, mod = 1e9 + 7;
int f[N][N], ne[N];
int main() {
int n;
cin >> n;
string p;
cin >> p;
int m = p.size();
p = " " + p;

for (int i = 2, j = 0; i <= m; ++i) {
while (j > 0 && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) ++j;
ne[i] = j;
}

f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < m; ++j) {//枚举处理前i - 1个字符后停留的状态
for (char c = 'a'; c <= 'z'; ++c) {
int q = j;
while (q > 0 && c != p[q + 1]) q = ne[q];
if (c == p[q + 1]) ++q;
if (q < m) f[i][q] = (f[i][q] + f[i - 1][j]) % mod;
}
}
}

int ans = 0;
for (int j = 0; j < m; ++j) ans = (ans + f[n][j]) % mod;
cout << ans;
}

1053. 修复DNA

题目描述

生物学家终于发明了修复DNA的技术,能够将包含各种遗传疾病的DNA片段进行修复。

为了简单起见,DNA看作是一个由’A’, ‘G’ , ‘C’ , ‘T’构成的字符串。

修复技术就是通过改变字符串中的一些字符,从而消除字符串中包含的致病片段。

例如,我们可以通过改变两个字符,将DNA片段”AAGCAG”变为”AGGCAC”,从而使得DNA片段中不再包含致病片段”AAG”,”AGC”,”CAG”,以达到修复该DNA片段的目的。

需注意,被修复的DNA片段中,仍然只能包含字符’A’, ‘G’ , ‘C’ , ‘T’。

请你帮助生物学家修复给定的DNA片段,并且修复过程中改变的字符数量要尽可能的少。

输入格式

输入包含多组测试数据。

每组数据第一行包含整数N,表示致病DNA片段的数量。

接下来N行,每行包含一个长度不超过20的非空字符串,字符串中仅包含字符’A’, ‘G’ , ‘C’ , ‘T’,用以表示致病DNA片段。

再一行,包含一个长度不超过1000的非空字符串,字符串中仅包含字符’A’, ‘G’ , ‘C’ , ‘T’,用以表示待修复DNA片段。

最后一组测试数据后面跟一行,包含一个0,表示输入结束。

输出格式

每组数据输出一个结果,每个结果占一行。

输入形如”Case x: y”,其中x为测试数据编号(从1开始),y为修复过程中所需改变的字符数量的最小值,如果无法修复给定DNA片段,则y为”-1”。

数据范围

1N501 \le N \le 50

输入样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
AAA
AAG
AAAG
2
A
TG
TGAATG
4
A
G
C
T
AGT
0

输出样例

1
2
3
Case 1: 1
Case 2: 4
Case 3: -1

算法分析

Solution