最大字段和(与a[i]有关

这个有点意思。

核心思路(Kadane 算法):“边走边选”

想象你从第一个数开始,逐个往后走,每走到一个数,只做两个选择:

  • 选:把当前数加入 “之前选的那段”,继续累加;
  • 不选:扔掉之前选的所有数,从当前数重新开始。

如果原来的dp[i-1]加上当前的a[i]得到的结果,结果还没有当前的a[i]大,那就从当前的开始选(用来解决正负的问题)

  • 每次选完后,记录一下 “当前能凑出的最大和”,最后全局最大的那个就是答案。

代码里的关键变量(对应思路)

  • a[i]:存储原始的数字数组;
  • dp[i]:表示 “以第 i 个数结尾的连续子数组的最大和”(比如dp[3]就是以第 4 个数结尾的最大和);
  • max_sum:记录遍历过程中所有dp[i]的最大值(最终答案)

二、通俗解释代码的执行步骤

以测试用例 5 -2 1 -3 4 -1 为例(n=5,数组是[-2,1,-3,4,-1]),一步步看代码怎么跑:

步骤 1:初始化

  • 读取 n=5,读取数组a = [-2,1,-3,4,-1]
  • dp[0] = a[0] = -2(第一个数结尾的子数组只能是它自己);
  • max_sum = -2(初始最大和就是第一个数)。

步骤 2:遍历第 2 个数(i=1,a [1]=1)

  • 做选择:

    • 选:dp[0]+a[1] = -2+1 = -1
    • 不选:直接取a[1] = 1
    • 选大的那个 → dp[1] = max(-1,1) = 1
  • 更新max_sum:1 > -2 → max_sum = 1

步骤 3:遍历第 3 个数(i=2,a [2]=-3)

  • 做选择:

    • 选:dp[1]+a[2] = 1+(-3) = -2
    • 不选:直接取a[2] = -3
    • 选大的 → dp[2] = max(-2,-3) = -2
  • 更新max_sum:-2 < 1 → max_sum还是 1。

步骤 4:遍历第 4 个数(i=3,a [3]=4)

  • 做选择:

    • 选:dp[2]+a[3] = -2+4 = 2
    • 不选:直接取a[3] = 4
    • 选大的 → dp[3] = max(2,4) = 4
  • 更新max_sum:4 > 1 → max_sum = 4

步骤 5:遍历第 5 个数(i=4,a [4]=-1)

  • 做选择:

    • 选:dp[3]+a[4] = 4+(-1) = 3
    • 不选:直接取a[4] = -1
    • 选大的 → dp[4] = max(3,-1) = 3
  • 更新max_sum:3 < 4 → max_sum还是 4。

步骤 6:输出结果

最终max_sum=4,就是答案(对应子数组[4])。

五、核心逻辑一句话总结

二选一(延续前序子数组 / 重新开始)

每走到一个数,就判断 “带着前面的数一起算” 和 “重新开始算” 哪个更划算,选划算的那个,同时记录过程中出现过的最大值,就是最终答案。

六、补充:代码为什么能跑快?

整个过程只遍历数组一次,时间是 O (n)(n 个数走 n 步),不管数组多长,都能很快算出结果,这也是 Kadane 算法的核心优势(比暴力枚举所有子数组的 O (n²) 快得多)。

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
#include <bits/stdc++.h>

using namespace std;

long long dp[10000001];
long long a[10000001];
long long maxx;

int main ()
{
int n;
while(cin>>n){
for(int i=0;i<n;i++){
cin>>a[i];
}
dp[0]=a[0];
maxx=a[0];
for(int i=1;i<n;i++){
//加入新的值之后,相比原来是否减小了?
dp[i]=max(dp[i-1]+a[i],a[i]);
//就算是全负数,这句也能记录下最大的负数
if(dp[i]>maxx){
maxx=dp[i];
}
}
cout<<maxx<<endl;
}

}

字符串区间翻转问题

原字符串 1 的个数+收益数组的最大子数组和

输入样例:41001

  • 原字符串 1 的个数:count_1 = 2(第 1 位和第 4 位是 1)。
  • 收益数组(最大字段和):[-1, 1, 1, -1]
  • Kadane 算法求最大子数组和:子数组 [1,1](对应原字符串第 2、3 位),收益为 2。
  • 最终结果:2 + 2 = 4,与样例输出一致。

最长递增子序列数量/求和、最长连续递增子序列、最长连续子串且求出这一串元素(与dp[i]有关

不连续递增⼦序列的跟前0-i 个状态有关,连续递增的⼦序列只跟前⼀个状态有关

img

一般都会问数量,但如果让你把具体是哪些数组成了这个最长子序列/最长子串,要使用start和end记录开始和结束的下表

dp含义:以dpi结尾的序列,最长的子序列是多少

递推式:dpi=max(dpj+1,dpi);

初始化:dp0=1

遍历顺序:

1
2
3
4
5
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(dp[j]<dp[i]){
dp[i]=max(dp[j]+1,dp[i])
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
#include <bits/stdc++.h>

using namespace std;

int main ()
{
int nums[10]={0,1,0,3,2,3};
int dp[10];
dp[0]=1;
for(int i=0;i<6;i++){
//数量初始化
dp[i]=1;
//求和初始化
for(int i = 0; i < n; i++){
dp[i] = a[i];
}
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
//数量
dp[i]=max(dp[j]+1,dp[i]);
//求和
dp[i]=max(dp[j]+nums[i],dp[i]);
}
}
}
int result=dp[0];
for(int i=0;i<6;i++){
result=max(result,dp[i]);
}
cout<<result;
}

那么这一串元素要怎么求呢

对于开头,我们必须使用一个start数组,vector start(k+1);并初始化start[0] = 0;

img

最长公共子序列数量、最长公共子序列输出

一定注意数组初始化的问题,尤其是开数组的时候,要么用vector<int> dp(a+1,vector(b+1,0).要么直接开一个超大的静态数组,然后memset(dp,0,sizeof(dp))

1
2
3
4
int dp[5][5] = {0}; // 合法!C++支持:第一个元素置0,其余元素默认初始化为0

int l1=3, l2=4;
int dp[l2+1][l1+1] = {0}; // 非法!C++标准不允许,且编译器扩展支持时也仅能初始化第一个元素,其余是随机垃圾值

img

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 <bits/stdc++.h>

using namespace std;

int main ()
{
string s1,s2;
//用空格隔开的话就直接cin,如果需要读入空格采用getline
while(cin>>s1>>s2){
int l1=s1.size(),l2=s2.size();
//这里使用l1+1是因为最外面还有一圈0,只开l1的话不够用
vector<vector<int>> dp(l1 + 1, vector<int>(l2 + 1, 0));
for(int i=1;i<=l1;i++){
for(int j=1;j<=l2;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
//这里不用加1
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
}
cout<<dp[l1][l2]<<endl;
}
}

最长子序列输出1730

这就是最长和连续最长的区别// 如果字符不匹配,当前连续长度重置为0

思路是记录下end_index,获得字符串长度m后,m-end__index既是开头,然后使用substr截取出来输出

这里有一点要注意,如果是输出最后一个的话,则比较的条件变成>=,这样即使是一样的也会取到最后一个

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
35
#include <bits/stdc++.h>

using namespace std;

int main ()
{
string s1,s2;
while(cin>>s1>>s2){
int l1=s1.size(),l2=s2.size();
vector<vector<int>> dp(l1 + 1, vector<int>(l2 + 1, 0));
int max_len =0;
int end_idx=-1;
for(int i=1;i<=l1;i++){
for(int j=1;j<=l2;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
if(dp[i][j]>=max_len){
max_len=dp[i][j];
end_idx=i;
}
}
// 如果字符不匹配,当前连续长度重置为0
else{
dp[i][j] = 0;
}
}
}
cout << max_len << endl;
if(max_len>0){
int start=end_idx-max_len;
string re=s1.substr(start,max_len);
cout<<re<<endl;
}
}
}

最长连续重复子数组

img

整数拆分

dp含义:乘积最大为多少

递推式:dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]))

由于动态数组初始化全部为零,初始dp[i]=0,外层max判断是否要更新为最新值

max(j*(i-j),j*dp[i-j]),前者为不拆,后者为拆

初始化:dp0=0,dp1=0(题目给的范围里这两个没意义),dp[2]=1

遍历顺序:

1
2
3
for(int i=2;i<=n;i++){
for(int j=1;j<=i/2;j++){
递推式

简单01背包问题

dp[i][j] 表示从下标为**[0-i]的物品⾥任意取,放进容量为j**的背

包,价值总和最⼤是多少

  1. 确定dp数组以及下标的含义
    对于背包问题,有⼀种写法, 是使⽤⼆维数组,即dp[i][j] 表示从下标为[0-i]的物品⾥任意取,放进容量为j的背
    包,价值总和最⼤是多少。
    只看这个⼆维数组的定义,⼤家⼀定会有点懵,看下⾯这个图:
    要时刻记着这个dp数组的含义,下⾯的⼀些步骤都围绕这dp数组的含义进⾏的,如果哪⾥看懵了,就来回顾⼀下i
    代表什么,j⼜代表什么。
  2. 确定递推公式
    再回顾⼀下dp[i][j]的含义:从下标为[0-i]的物品⾥任意取,放进容量为j的背包,价值总和最⼤是多少。
    那么可以有两个⽅向推出来dp[i][j],
    不放物品i:由dp[i - 1][j]推出,即背包容量为j,⾥⾯不放物品i的最⼤价值,此时dp[i][j]就是dp[i - 1][j]。(其
    实就是当物品i的重量⼤于背包j的重量时,物品i⽆法放进背包中,所以背包内的价值依然和前⾯相同。)
    放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最
    ⼤价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最⼤价值
    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
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
35
36
37
38
39
40
41
#include <iostream>
#include <algorithm> // max函数需要的头文件
#include <cstring> // memset需要的头文件
using namespace std;

// 定义数组(保持你原有的维度)
int dp[21][1010]; // dp[i][j]:前i个物品装j容量的最大价值
int w[21], c[21]; // w[i]:价值,c[i]:体积

int main(){
int N,V;
cin>>N>>V; // 输入:物品数量N,背包总容量V

// 输入物品的价值(w[i])和体积(c[i])
for(int i=1;i<=N;++i){
cin>>w[i]>>c[i];
}

// ========== 新增:第一行(第一个物品)初始化逻辑 ==========
// 第一个物品是i=1,遍历容量j从c[1]到V,初始化为第一个物品的价值w[1]
for(int j = c[1]; j <= V; j++){
dp[1][j] = w[1]; // 只有第一个物品时,能装下则价值为w[1],装不下则保持默认0
}
// ==========================================================

// 遍历剩余物品(从第2个开始,i从2到N)
for(int i=2;i<=N;++i){ // 注意:i从2开始(第一个物品已初始化)
for(int j=0;j<=V;++j){ // 遍历背包的所有可能容量(从0到V)
//放得下,但要考虑选不选
if(j>=c[i]){ // 能装下第i个物品
dp[i][j] = max(dp[i-1][j-c[i]] + w[i], dp[i-1][j]);
}
else{ // 装不下第i个物品
dp[i][j] = dp[i-1][j];
}
}
}

cout<<dp[N][V]<<endl; // 输出前N个物品装入容量V的最大价值
return 0;
}

1086采药

此类题一定要让数组从1开始,不然会有很多麻烦

想一想数组第一行初始化的时候要让i等于多少?

遍历的时候ij的范围是多少?比较的时候j要不要加等于号?

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 <bits/stdc++.h>

using namespace std;

int main ()
{
int t,m;
while(cin>>t>>m){
vector<int> att(m+1);
vector<int> amm(m+1);
vector<vector<int>> dp(m + 1, vector<int>(t + 1, 0));

for(int i=1;i<=m;i++){
cin>>att[i]>>amm[i];
}

for(int i=att[1];i<=t;i++){
dp[1][i]=amm[1];
}

for(int i=2;i<=m;i++){
for(int j=0;j<=t;j++){
if(j>=att[i]){
dp[i][j]=max(dp[i][j],max(dp[i-1][j],dp[i-1][j-att[i]]+amm[i]));
}
else
dp[i][j]=dp[i-1][j];
}
}

cout<<dp[m][t]<<endl;
}

}

完全背包

与01背包的区别是不用初始化

而且如果选则加入的话,递推式变成 dp[i][j]=max(dp[i][j],max(dp[i-1][j],dp[i][j-att[i]]+amm[i]));原因是01背包选完i后只能从i-1开始选,而完全背包选完i后仍然可以选择i

初始化时,vector<vector>代表是开一个二维数组,里面的每个一维数组也是vector类型。有n+1行,每行是一个vector类型,长度为w+1,初始化为0

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 <bits/stdc++.h>

using namespace std;

int main ()
{
int n,w;
while(cin>>n>>w){
vector<int> att(n+1);
vector<int> amm(n+1);
vector<vector<int>> dp(n + 1, vector<int>(w + 1, 0));
for(int i=1;i<=n;i++){
cin>>att[i]>>amm[i];
}
for(int i=1;i<=n;i++){
for(int j=0;j<=w;j++){
if(j>=att[i]){
dp[i][j]=max(dp[i][j],max(dp[i-1][j],dp[i][j-att[i]]+amm[i]));
}
else
dp[i][j]=dp[i-1][j];
}
}

cout<<dp[n][w]<<endl;
}

}

字符串编辑距离

回溯算法

模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void backtracking(参数) {
// 1. 终止条件:走到决策树的叶子节点,找到一组有效解
if (终止条件) {
存放结果; // 将当前有效路径存入结果集
return; // 回溯,回到上一层继续尝试其他选择
}

// 2. 遍历本层的所有可选元素(决策树的当前层分支)
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点; // 做出选择:将当前元素加入路径
backtracking(路径,选择列表); // 递归:深入下一层,继续选择
回溯,撤销处理结果; // 撤销选择:回到当前层,尝试下一个元素
}
}

计算组合数(1234里面挑两个)(回溯,用了对数组push_back)

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
35
36
#include<bits/stdc++.h>
using namespace std;

vector<vector<int>> result; // 存储所有符合条件的组合
vector<int> path; // 存储当前路径的组合

// 回溯函数:n-总数,k-选取个数,s_idx-当前起始索引(从1开始)
void backt(int n, int k, int s_idx) {
// 终止条件:当前组合长度等于k,保存结果并返回
if (path.size() == k) {
result.push_back(path);
return; // 必须return,避免后续无效循环
}
// 剪枝优化:i的上限可缩小为n - (k - path.size()) + 1,减少循环次数
for (int i = s_idx; i <= n; i++) { // 终止条件改为i<=n,包含数字n
path.push_back(i); // 将当前数字加入路径
backt(n, k, i + 1); // 递归:起始索引+1,避免重复
path.pop_back(); // 回溯:撤销当前选择
}
}

int main() {
int n = 4, k = 2; // 从1~4中选2个数字的组合
backt(n, k, 1); // 起始索引从1开始(对应数字1)

// 遍历输出所有组合
cout << "所有" << k << "个数的组合:" << endl;
for (auto& vec : result) {
for (int num : vec) {
cout << num << " ";
}
cout << endl;
}

return 0;
}

合并石子

img

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<bits/stdc++.h>
using namespace std;

int a[1001],dp[1001][1001],sum[1001];
int main() {
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]+=sum[i-1]+a[i];
}
//枚举区间长度
for(int len=2;len<=n;len++){
//枚举起点
for(int i=1;i<=n-len+1;i++){
//终点
int j=i+len-1;
dp[i][j]=1e8;
//区间dp
for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
}
}
cout<<dp[1][n];
}

img

字符串编辑距离

问题描述:给定两个字符串A和B,求字符串A至少经过多少步字符操作变成字符串B。

我们先以一个例子分析,比如eat变成tea。对于第一个字符,e != a,所以要想让这两个字符相等,有三种可以选择的办法

  • 修改字符,将e直接变成a,需要走1步。
  • 插入字符,在e的前面插入a,也需要走1步。
  • 删除字符,将e删除,然后比较后面的与a,也需要走1步。
1
2
3
4
5
6
7
8
9
原理:
给出一个初始化矩阵,逐位比较两个字符串,待修改的矩阵对应为行,被比较的矩阵对应位列
若字符串对应的位相同,则让该矩阵位直接让这一位的矩阵的等于左上对角的值
否则,有三种情况,分别是:
①:等于上面矩阵位加一
②:等于左上角矩阵位加一
③:等于左边矩阵位加一
最后该矩阵位取上面三种情况中最小的一中。
最右下角的一位保存的就是字符串编辑距离

初始化如下所示:因为相当于把字符串全部删除所需要的步数

a b c d
0 1 2 3 4
a 1
c 2
d 3

更新过程:

相等:抄左上角的

不相等:min(左边加一,上面加一,左上角加一)

a b c d
0 1 2 3 4
a 1 0 1 2
c 2 1 1 1
d 3 2 2 2