#include<iostream>
using namespace std;
/*常见动态规划模型 --子序列匹配 */
/*
感悟: dp是很好写的,主要是总结 !状态转移方程 !
代码错误是由多练多错总结记忆,才快速反应的,没遇到过的错误只会无从下手
*/
/*最大子段和
给定一段数组,其中一段连续的序列称为一个子段,(只考虑非空子段)
全部非正数,则取其中的元素的最大值
可以只扫描1次,sum统计 和 ans更新 ,前缀为负数,舍弃
输入:
6
-2 11 -4 13 -5 -2
输出:
20
*/
const int inf_01 = 1000000000;
int ans_01 = -inf_01; //最小保证更新
int sum_01,N_01;
int num_01[101];
void test_01() { //O(N_01)
cin >> N_01;
for(int i = 0 ; i < N_01; i++) {
cin >> num_01[i];
ans_01 = max(ans_01,num_01[i]); //每位元素判断更新
}
if(ans_01 <= 0) {
cout<< ans_01 << endl;//都是负数,选元素的最大值
} else {
sum_01 = 0;
for(int i = 0; i < N_01 ; i++) {
if(sum_01 + num_01[i] < 0) { //前缀是负数,那么扔掉这个前缀一定更优
sum_01 = 0; //不选
} else {
sum_01 += num_01[i];
}
ans_01 = max(ans_01,sum_01); //ans_01更新最大值
}
cout<< ans_01 << endl;
}
return;
}
/*最长上升子序列 LIS
在原序列取任意多项式 ,不改变他们在原来数列的先后次序 !!! ,得到的序列称为原序列的子序列
最长上升子序列:不断递增的子序列(数值从高到低)
如果有 a[j] < a[i] ,就把 第i项接在 第j项后面
状态转移方程: dp[i] = max( dp[i],dp[j] + 1 ), 1<= j <= i && a[j] < a[i]
输出最长上升子序列的长度
dp数组里的最大值就是最长上升子序列的长度了 !!!
输入:
6
3 2 6 1 4 5
输出:
3
*/
int dp_02[101],a_02[101],n_02;
int LIS_02() {
int ans = 0;
for(int i = 1; i <= n_02; i++) {
dp_02[i] = 1;
for(int j = 1; j < i; j++) {
if(a_02[j] < a_02[i]) {
dp_02[i] = max( dp_02[i] , dp_02[j] + 1 ); //求子序列最大长度的dp_02方程
}
}
ans = max( ans , dp_02[i] );
}
return ans;
}
void test_02() {
cin >> n_02;
for(int i = 1; i <= n_02; i++) {
cin >> a_02[i];
}
cout << LIS_02() <<endl;
return;
}
/*最长公共子序列 ,给定两个序列s1和s2,求二者公共子序列s3的最长的长度
s1的前i个字符和s2的前j个字符 的最长公共子序列长度 记作lcs[i][j]
如果s1的第i项 s2的第j项 相同 ,那么s1[i] ,s2[j] 作为公共子序列 的末尾
则 lcs[i][j] = lcs[i - 1][j - 1] + 1 //长度+1
也可以不让 s1[i] ,s2[j] 作为公共子序列 的末尾
则 lcs[i][j] = max( lcs[i][j - 1],lcs[i - 1][j] )
可证: max( lcs[i][j - 1],lcs[i - 1][j] ) <= lcs[i - 1][j - 1] + 1
即 lcs[i][j] = lcs[i - 1][j - 1] + 1 , s1[i] == s2[j]
lcs[i][j] = max( lcs[i][j - 1],lcs[i - 1][j] ) , s1[i] != s2[j]
输入:
abcdefgh
acjlfabhh
输出:
4
*/
#include<cstring>
int dp_03[110][110];
void test_03() {
string a,b;
memset(dp_03,0,sizeof(dp_03) );
cin >> a >> b;
int lena = a.size();
int lenb = b.size();
for(int i = 1; i <= lena; i++) {
for(int j = 1; j <= lenb; j++) {
if(a[i - 1] == b[j - 1]) {
dp_03[i][j] = dp_03[i - 1][j - 1] + 1;//i和j可以匹配,加上前面的匹配个数 再加当前匹配 1 位 转移
} else { //当前位置不相等,不让S1[i]和 S2[j] 作为末尾
//没有匹配,(前面不会产生新的匹配),考虑分别省略掉一位,看结果是多少 ,转移! // lcs考虑子序列最后一位一定在这位置上
dp_03[i][j] = max(dp_03[i - 1][j] ,dp_03[i][j - 1] ) ;
}
}
}
cout << dp_03[lena][lenb] << endl;
return;
}
/*给定两个字符串 S 和 T ,对于字符串T ,我们进行下面三种操作
1.在任意位置添加任意字符
2.删除存在的任意字符
3.将一个任意字符修改为另一个任意字符
问经过多少次操作可以将字符串T变成字符串S (最小)
设S的长度为m ,T的长度为n
简单想法:把T全删了,添加S的全部字符 O(m + n)
优化:把T的长度变为m , 再操作,操作次数最多为 |m - n| + m
(发现搜索不可行,搜索的空间时指数级的,取决于S的字符种类 ,比如S由小写字母组成 ,复杂度就为 O(26 ^ m) )
换一种角度:字符串对齐
1) S[i] == T[j] , 答案就是 f(i - 1,j - 1)
2) S[i] != T[j] , 那么在当前位置进行修改,因此答案就是 f(i - 1,j - 1) + 1 //操作 + 1
3) S的前i位和T的前j-1位对齐之后,在当前位置需要执行一次添加操作 ,因此答案为:f(i,j - 1) + 1
4) S的前i - 1位和T的前j位对齐之后,在当前位置需要执行一次删除操作 ,因此答案为:f(i - 1,j) + 1
递推式:
dp[i][j] = dp_04[i][j] = dp_04[i - 1][j - 1]; ,S[i] == T[j]
dp[i][j] = min( dp_04[i - 1][j - 1] , min( dp_04[i][j - 1] , dp_04[i - 1][j] ) ) + 1; ,S[i] != T[j]
边界处理:(对于S的前0个字符,只能把T中的字符全部删掉,同理,对于T中的前0个字符,S只能选择把T中的添加上S当前长度的字符)
dp[0][j] = j;
dp[i][0] = i;
时间复杂度、空间复杂度均为 O(m + n)
输入:
abcd
acef
输出:
3
*/
int dp_04[110][110];
string a_04,b_04;
void test_04() {
cin >> a_04 >> b_04;
int lena = a_04.size();
int lenb = b_04.size();
for(int i = 1; i <= lena; i++) {
dp_04[i][0] = i;
}
for(int i = 1; i <= lenb; i++) {
dp_04[0][i] = i;
}
for(int i = 1; i <= lena; i++) {
for(int j = 1; j <= lenb; j++) {
if(a_04[i - 1] == b_04[j - 1]) {
dp_04[i][j] = dp_04[i - 1][j - 1];//对应位匹配,直接判断下一位,不用操作
} else { //对应位不匹配 , 找操作少的省略前一位的比较判断
dp_04[i][j] = min( dp_04[i - 1][j - 1] , min( dp_04[i][j - 1] , dp_04[i - 1][j] ) ) + 1;
}
}
}
cout << dp_04[lena][lenb] << endl;
return;
}
/*常见动态规划模型练习题 -- 自己盲打一遍 !!! -- 口述大致思路对应代码 */
/*一排数,求最大非空子段和 test_01
*/
/*一个矩阵 , 求最大非空子矩阵和
思路:枚举 上下边界 , 转换成一维
输入:
3 3
1 -2 3
-4 5 -6
7 -8 9
输出:
9
*/
long long num_05[401][401]; // 1<= N,M <= 400 , -1e9 <= ans <= 1e9
long long presum_05[401][401]; //前一个状态sum
void test_05() {
int N,M;
long long sum,ans;
cin >> N >> M;
ans = -1000000001;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= M; j++) {
cin >> num_05[i][j];
ans = max(ans,num_05[i][j]);
}
}
if(ans <= 0) {
cout << ans << endl;
} else {
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= M; j++) {
presum_05[i][j] = presum_05[i - 1][j] + num_05[i][j];
}
}
for(int i = 1; i <= N; i++) {
for(int j = i; j <= N; j++) { //上下边界 ,N行
sum = 0;
for(int k = 1; k <= M; k++) { //从左到右遍历
if(sum + presum_05[j][k] - presum_05[i - 1][k] < 0) {
sum = 0;
} else {
sum += presum_05[j][k] - presum_05[i - 1][k];
}
ans = max( ans,sum );
}
}
}
cout << ans << endl;
return;
}
}
/*环形矩阵的最大空子矩阵
枚举上下左右边界 , 分成4份
整体 - 中间
*/
/*跳木桩
高度h1 -- hn
第一步可以跳到任意一个木桩
之后只能往前跳(不能往回跳)到任意一个木桩 , 且能跳到的下一个木桩的高度 <= 当前木桩
计算最多能跳多少个木桩 (即最长不上升子序列!) --子序列 不改变原序列元素相对顺序 ,取若干项
输入:
7
3 2 6 1 4 5 0
输出:
4
*/
const int MAXN_06 = 1010;
int a_06[MAXN_06];
int dp_06[MAXN_06];
void test_06() {
int n;
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a_06[i];
}
int ans = 0;
for(int i = 0; i < n; i++) {
dp_06[i] = 1;//最开始最少单独一个
for(int j = 0; j < i; j++) { //每一段所有子序列遍历
if(a_06[j] >= a_06[i]) {//不上升子序列
dp_06[i] = max(dp_06[i] , dp_06[j] + 1 );//有不上升(即下降)+1 ,当前dp_06[i]与dp_06[j] + 1 比较,看会不会更长 ,更长就更新
}
}
ans = max(ans,dp_06[i]); //更新答案
}
cout << ans << endl;
return;
}
/*给定n个数 的A序列
删除任意元素
是A变成前一段上升,后一段下降 的序列 (先减后增)
求最少需要被删除的元素
从前往后做一遍不上升子序列子序列 ,再从后往前做一遍不上升子序列 (有最小值)
输入:
输出:
*/
const int MAXN_07 = 2010;
int a_07[MAXN_07];
int dp_07[2][MAXN_07];
void test_07() {
int n;
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a_07[i];
}
for(int i = 0; i < n; i++) {
dp_07[0][i] = 1;
for(int j = 0; j < i; j++) {
if(a_07[j] >= a_07[i]) {
dp_07[0][i] = max( dp_07[0][i] ,dp_07[0][j] + 1);
}
}
}
for(int i = n - 1; i >= 0; i--) {
dp_07[1][i] = 1;
for(int j = n - 1; j > i; j--) {
if(a_07[j] >= a_07[i]) {
dp_07[1][i] = max( dp_07[1][i] ,dp_07[1][j] + 1);
}
}
}
int ans = 0;
for(int i = 0; i < n; i++) {
ans = max(ans , dp_07[0][i] + dp_07[1][i] + 1 );
}
cout << n - ans << endl;
return;
}
/*
最大递增难度和 (上升子序列最大和)
输入:
3
1 3 2
输出:
4
*/
int a_08[1001];
long long sum_08[1001];
int ans_08;
void test_08() {
int n;
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a_08[i];
}
for(int i = 0; i < n; i++) {
sum_08[i] = a_08[i];//初始
for(int j = 0; j < i; j++) {
if(a_08[j] < a_08[i] && sum_08[j] + a_08[i] > sum_08[i]) { //转移条件 ,递增 && 难度和更大
sum_08[i] = sum_08[j] + a_08[i];
}
}
}
for(int i = 0; i < n; i++) {
if(sum_08[i] > ans_08) {
ans_08 = sum_08[i];
}
}
cout << ans_08 << endl;
return;
}
/*
最长公共子序列
输入:
computer
education
输出:
2
*/
#include<algorithm>
#include<cstdio>
#include<string>
const int MAXN_09 = 2010;
int dp_09[MAXN_09][MAXN_09];
void test_09(){
string a,b;
cin >> a>> b;
for(int i = 1;i <= a.size();i++){ //从1开始!
for(int j = 1;j <= b.size();j++){
if(a[i - 1] == b[j - 1] ){
dp_09[i][j] = dp_09[i - 1][j - 1] + 1;
}else{
dp_09[i][j] = max( dp_09[i - 1][j],dp_09[i][j - 1] );
}
}
}
cout << dp_09[a.size()][b.size()] << endl;
return;
}
/*字符串变回文串
等效改操作变相等串问题
即让s与s的反转 相等
如trit 变 tirit //可以添加一个i变回文串
输出最少添加次数
输入:
trit
输出:
1
*/
#include<algorithm>
#include<string>
int dp_10[3005][3005];
string s1_10,s2_10;
void test_10(){
cin >> s1_10;
s2_10 = s1_10;
reverse(s2_10.begin(),s2_10.end()); //反转 让两个字符串一样
for(int i = 1;i <= s1_10.size();i++){//从1开始好写
for(int j = 1;j <= s2_10.size();j++){
if(s1_10[i - 1] == s2_10[j - 1] ){//从0开始判断
dp_10[i][j] = dp_10[i - 1][j - 1] + 1; //对应位匹配 + 1
}else{
dp_10[i][j] = max( dp_10[i - 1][j],dp_10[i][j - 1] ); //各尝试前面的已经匹配的哪一个更长
}
}
}
cout<< s1_10.size() - dp_10[s1_10.size()][s2_10.size()] << endl; //dp_10[s1_10.size()][s2_10.size()]为最后状态 ,即取了最优解
return;
}
/*
被破坏的数据
第一行字符串A ,表示一个本被破坏之后的字符串
第二行字符串B ,表示第二个本上面的字符串
字符串均小于1000
可以增删改 ,最少的操作还原 A 变 B(备份)
套用 test_04()
输入:
aa
ab
输出:
1
*/
int dp_11[1001][1001];
void test_11(){
string A,B;
cin >> A;
cin >> B;
for(int i = 1; i < A.length();i++){ //初始
dp_11[i][0] = i;
}
for(int i = 1; i < B.length();i++){
dp_11[0][i] = i;
}
for(int i = 1;i <= A.length();i++){
for(int j = 1;j <= B.length();j++){
if(A[i - 1] == B[j - 1]){
dp_11[i][j] = dp_11[i - 1][j - 1]; //相等无需操作 ,判断下一位
}else{ //不相等,考虑改、 增 (i-1位 +1)、删 (删后少一位,Ai-1与Bj去比较决策)、 三种情况
dp_11[i][j] = min (dp_11[i - 1][j - 1] , min( dp_11[i][j - 1], dp_11[i - 1][j] ) ) + 1; //不相等回退 , 选最小次数的操作 + 本次操作
}
}
}
cout << dp_11[A.length()][B.length()] << endl; //最后遍历完,得到最优解的下标 [A.length()][B.length()]
return;
}
int main() {
test_11();
return 0;
}
蓝桥杯算法入门_11(动态规划 --子序列匹配)
于 2022-03-12 10:36:31 首次发布