蓝桥杯_动态规划_2 (线性DP 【数字三角形 + 最长上升子序列 】- 区间DP 【石子合并】+ 计数类DP【整数划分】)

文章目录

  • 898.数字三角形
  • 895. 最长上升子序列 O(n^2^)
  • 896.最长上升子序列 II O(nlogn)
  • 897.最长公共子序列
  • 902.最短编辑距离
  • 899. 编辑距离
  • 石子合并 【区间DP 】
  • 900. 整数划分 - 计数类DP

898.数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数n,表示数字三角形的层数。
接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1 ≤ n ≤ 500,
-10000 ≤ 三角形中的整数 ≤ 10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30

动态规划
	状态表示
		集合:**所有从起点,走到(i,j)的路径**
		属性:Max 
	状态计算(分类,从左边走下来/右边走下来的) 
		f[i][j] = max( f[i-1][j-1] + a[i][j] ,f[i-1][j] + a[i][j]);  加上当前点的坐标,比较取最大值
#include 
using namespace std;
#include

const int N = 510,INF = 1e9;

int n;
int a[N][N];
int f[N][N];

int main()
{
	
	scanf("%d",&n);
	
	
	for(int i = 1;i<= n;i++)
		for(int j = 1;j <= i;j++)
			scanf("%d",&a[i][j]);//dp数组从1开始,记录三角形走到每个位置的最大值 
	
	for(int i = 0;i<= n;i++)
		for(int j = 0;j <= i + 1;j++) //数字三角形初始化 i+1是因为dp比较时会越界,越出三角形,所以把哪些位置赋值负无穷  
			f[i][j] = -INF;
	
	f[1][1] = a[1][1];//起点位置 
	for(int i = 2;i <= n;i++)//i从第2行开始 
		for(int j = 1; j <= i;j++)
			f[i][j] = max(f[i-1][j-1] + a[i][j] , f[i-1][j] + a[i][j]);
			
	int res = -INF;
	for(int i = 1;i <= n;i++) res = max(res , f[n][i]);//最终答案要遍历一下最后一行,取走到最后一行的最大值中比较 再取最大值 
	
	cout << res << endl;
	
	return 0;
}



895. 最长上升子序列 O(n2)

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
-109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

动态规划
状态表示
集合:所有第i个数结尾的最长上升子序列
属性:Max - - 以i结尾长度最大值
状态计算
把数排列【a[i]>a[j],那么a[i]的最长序列长度为f[j]+1 (比j多1) 】
f[i] = max(f[i] , f[j] + 1) j = 0 ,1 , 2 … i - 1

简记:f[i] : 以第i个数结尾的最长上升子序列 LIS
if(a[j] < a[i]) f[i] = max(f[i] , f[j] + 1)

DP时间复杂度:O(n2)

#include 
using namespace std;
const int N = 1010;

int n;
int a[N],f[N];

int main()
{
	scanf("%d",&n);
	for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
	
	for(int i = 1;i <= n;i++)
	{
		f[i] = 1;
		for(int j = 1;j <= i;j++)
		{
			if(a[j] < a[i]) //满足条件,更新j的长度,比较长度 
				f[i] = max(f[i] , f[j] + 1);
		}
	}
	
	int res = 0;
	for(int i = 1;i <= n;i++) res = max(res , f[i]); //取最大长度 
	
	cout << res << endl; 
	
	return 0;
}




896.最长上升子序列 II O(nlogn)

题目描述:
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。第二行包含N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,-109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

【贪心+二分解法】维护严格单调上升序列q
解释的不错:每个LIS的最后一个数一定 > (长度小于这个LIS)的子序列 的最后一个数

简记:q[i]存所有小于a[i]的最大值,二分模板 : 找小于a[i]的max
#include
using namespace std;

const int N = 1e6+10;

int a[N];
int q[N];
int n;

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)  scanf("%d",&a[i]);
 
    q[0]=-2e9;//绝对最小,可被更新
    int len=0;

    for(int i=0;i<n;i++)
    {
        int l=0,r=len;
        while(l<r)
        {
            int mid=(l+r+1)>>1;   //单调找小于a[i]的max
            if(q[mid]<a[i]) l=mid;  //发现满足时,ans在mid右边  ,选取模拟二
            else r=mid-1; 
        }
        len=max(len,r+1);  
        q[r+1]=a[i]; 
    }

    printf("%d\n",len);
    return 0;
}
这是哪一题....... ?懵
/*
#include
using namespace std;
const int N = 1010;

int n;
int a[N],f[N],g[N];

int main()
{
	scanf("%d",&n);
	for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
	
	for(int i = 1;i <= n;i++)
	{
		f[i] = 1;
		g[i] = 0;//可以知道f是从什么状态转移过来的 ,递归回去逆序输出序列
		for(int j = 1;j <= i;j++)
		{
			if(a[j] < a[i]) //满足条件,更新j的长度,比较长度 
				if(f[i] < f[j] + 1)
				{ 
				  f[i] = f[j] + 1;
				  g[i] = j; //保存序列 
				} 
		}
	}
	
	int k = 1;	
	for(int i = 1;i <= n;i++)//最后的第k个结尾的长度,取最长 
		if(f[k] < f[i])	
			k = i;//以k结尾的 为最长上升子序列 
			
	printf("%d\n",f[k]); //输出最长长度f[k] 
		
	for(int i = 0,len = f[k]; i < len ;i++ ) //逆序输出最长上升子序列 
	{ 
		printf("%d ",a[k]);
		k = g[k];//保存f[k]从哪个状态转移过来 【保存序列】 ,输出过程类似递归查找 
	} 
	//可以存一个数组,再for(int i = f[k];i > 0;i --) cout << tmp[i] <<" ";逆序输出,变正序 
	return 0;
}



897.最长公共子序列

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数N和M。
第二行包含一个长度为N的字符串,表示字符串A。
第三行包含一个长度为M的字符串,表示字符串B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N,M ≤ 1000
输入样例:
4 5
acbd
abedc
输出样例:
3
蓝桥杯_动态规划_2 (线性DP 【数字三角形 + 最长上升子序列 】- 区间DP 【石子合并】+ 计数类DP【整数划分】)_第1张图片

动态规划
状态表示f[i,j]
集合 第一个序列的前i个字母,和第二个序列的前j个字母的子序列
属性 Max
状态计算
00 01 10 11 (ai 和 bj 包括/不包括在最长子序列中 的四种情况–> 转移到当前状态 取最大值max)
00【ai和bj均不出现在公共子序列中】: f[i-1][j-1]
11:f[i-1][j-1] + 1 , 01: f[i - 1][j] 包含了【ai不出现在公共子序列当中,bj出现的情况】 , 10: f[i][j-1]
包含可以代替的原因:我们求的是最大公共长度,即重复没有关系,答案不变 【因为f[i-1][j-1]被包含在01,10这两种情况中,所有不用比较】
f[i][j] = max(f[i-1][j] , f[i][j - 1] );
if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);

杂扩:y总char改int的分析(逐位赋值,拼接成整数)

简记:两个序列,每个位选/不选 (0 / 1) * (0 / 1)  [四种分类]
【用到下标i-1,所以从1开始】 char a[N]
情况包含有重复,分开比,max不影响(类似max(a,b) , max(b,c) )
			f[i][j] = max(f[i-1][j] , f[i][j - 1] );
			if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);	
#include 
#include
using namespace std;

const int N = 1010;

int n,m;
char a[N],b[N];
int f[N][N];

int main()
{
	scanf("%d%d",&n,&m);
	scanf("%s%s",a + 1 , b + 1);
	
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= m;j++)
		{
				f[i][j] = max(f[i-1][j] , f[i][j - 1] );
				if(a[i] == b[j]) f[i][j] = max(f[i][j] , f[i-1][j-1] + 1);	
		}	
	
	cout << f[n][m] <<endl;
	
	return 0;
}



902.最短编辑距离

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

删除–将字符串 A 中的某个字符删除。
插入–在字符串 A 的某个位置插入某个字符。
替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式
第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大写字母。

输出格式
输出一个整数,表示最少操作次数。

数据范围
1≤n,m≤1000

输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC

输出样例:
4
蓝桥杯_动态规划_2 (线性DP 【数字三角形 + 最长上升子序列 】- 区间DP 【石子合并】+ 计数类DP【整数划分】)_第2张图片

简记:所有将a[1~i]变成b[1~j]的操作方式
初始化
for (int i = 0; i <= m; i ++ ) f[0][i] = i; // 只添加,a的前0个添加i个后与b的前i个字母相等
for (int i = 0; i <= n; i ++ ) f[i][0] = i; // 只删除,a的前i个删除后变成与b的前0个相等
  删除和插入操作比较
        f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
  替换操作比较
        if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);  替换改f[i-1][j-1]+0/1次,a[i]==b[j]则最后一次不用改,不加操作次数
        else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
    }
#include 
#include 

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
    scanf("%d%s", &n, a + 1);//用a+1地址读入,字符串下标从1开始【用i-1,不用特判边界】
    scanf("%d%s", &m, b + 1);

    // 初始化
    for (int i = 0; i <= m; i ++ ) f[0][i] = i; // a的前0个与吧的前i个字母:只用添加使得前i个字母相等
    for (int i = 0; i <= n; i ++ ) f[i][0] = i; // 只删除,a的前i个删除后变成与b的前0个相等

    for (int i = 1; i <= n; i ++ ) 
        for (int j = 1; j <= m; j ++ )
        {
            // 删除和插入操作比较
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            // 替换操作比较
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//若相等就是最后一次,不用再加
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }

    printf("%d\n", f[n][m]);

    return 0;
}

899. 编辑距离

给定n 个长度不超过10 的字符串和m 次询问,每次询问含一个字符串和一个操作次数m ,问这n 个字符串里有多少个可以通过不超过m 次操作变成询问的那个字符串。单个字符的插入、删除或替换算一次操作。
输入格式:
第一行包含两个整数n 和m 。接下来n 行,每行包含一个字符串,表示给定的字符串。再接下来m 行,每行包含一个字符串和一个整数,表示一次询问。字符串中只包含小写字母,且长度均不超过10 。
输出格式:
输出共m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围:
1 ≤ n , m ≤ 1000
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例:
1
3

#include 
#include 
using namespace std;

const int N = 15, M = 1010;

int n, m;
int f[N][N];
char str[M][N];//字符串数组  (取字符串:str[i])

int edit_dis(char a[], char b[]) { //编辑距离DP模板
    int la = strlen(a), lb = strlen(b);  //长度
    for (int i = 0; i <= lb; i++) f[0][i] = i;
    for (int i = 0; i <= la; i++) f[i][0] = i;

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

    return f[la][lb];
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> str[i];

    while (m--) {//询问n个串中有几个满足
        char s[N];
        int limit;
        scanf("%s%d", s, &lim);   //引用型 , char数组可直接%s读入   !!!

        int res = 0;
        for (int i = 0; i < n; i++) 
            if (edit_dis(str[i], s) <= limit)  //能在限制次数limit内从a[1~i]->b[1~j]的方案数
                res++;
        
        cout << res << endl;
    }

    return 0;
}

石子合并 【区间DP 】

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22

动态规划
状态表示 f[i][j]
集合:所有将第i堆石子到第j堆石子的合并方式
属性 Min
状态计算
所有石子最后一步都是两边的合并【[i,j]区间 i左j右 最后为 f[i][k] 与 f[k+1][j]合并】,【哪些状态转移到最后一步】
每一类的最小代价取Min
f[i][j] = min(f[i][k] + f[k +1][j] +s[j] - s[i - 1]) (k = i - j + 1)
时间复杂度: O(n3) : 3003 < 1e8 ,可行

简记:前缀和s[j] - s[i-1]:i->j区间的石子合并值    len = r - l + 1   (从区间长度为2 , len = 2开始)
for (int i = 1;i + len - 1 <= n; i++) {
        int j = i + len - 1; f[i][j] = 0x3f3f3f3f;   注意求min,初始化INF
for (int k = i; k <= j; k++) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);   k遍历[l,r]
#include 
using namespace std;
const int N = 310;

int n;
int s[N]; 
int f[N][N];
int main() {
    scanf("%d", &n);
    for (int i = 1;i <= n; i++) scanf("%d", &s[i]), s[i] += s[i - 1] ;   //以分号结束算一条语句 

    //memset(dp, 0, sizeof dp);
    for (int len = 2; len <= n; len++)  //len == r - l + 1
        for (int i = 1;i + len - 1 <= n; i++) {
            int j = i + len - 1; f[i][j] = 0x3f3f3f3f;
            for (int k = i; k <= j; k++) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);// 搬动[l,r] 的代价为 s[r] - s[l - 1] 
        }
    printf("%d\n", f[1][n]);    //1 -> n 的最小代价
    return 0;
} 


旧版:

#include
using namespace std;

const int N = 310;

int n;
int s[N]; //前缀和数组 , 代价值用利用前缀和数组求区间和 计算 
int f[N][N];

int main()
{
	
	scanf("%d",&n);
	for(int i = 1;i <= n;i++)scanf("%d",&s[i]);
	 
	for(int i = 1;i <= n;i++) s[i] += s[i - 1];  //初始化 s[i] = s[i - 1] + a[i] (这里因为a[i]已经存入s[i]中,所以这样写等效) 
	
	for(int len = 2;len <= n;len ++) //len = 1代价是0,从len = 2开始即可 ,区间长度 
		for(int i = 1;i + len - 1 <= n;i ++) //区间的左端点 
		{
			int l = i,r = i + len - 1; 
			f[l][r] = 0x3f3f3f3f;//初始化,否则每次都是0 
			for(int k = l;k < r;k ++) //DP计算 
				f[l][r] = min(f[l][r] , f[l][k] + f[k + 1][r] + s[r] - s[l - 1]); // 搬动[l,r] 的代价为 s[r] - s[l - 1] 
		}
	
	cout << f[1][n] << endl;//将第1堆~第n堆[1,n]区间合并的最小代价为f[1][n] 
	
	return 0;
}

递归的DP --> 又称为记忆化搜索

900. 整数划分 - 计数类DP

1、题目描述
一个正整数n可以表示成若干个正整数之和,形如:n=n1+n2+…+nk 其中n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数n的一种划分。
现在给定一个正整数n,请你求出n共有多少种不同的划分方法。
输入格式
共一行,包含一个整数n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对109+7109+7取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7

完全背包写法:
	从1~i中选,总体积恰好是j的选法(化简推导):f[i][j] = f[i-1][j] + f[i][j-1] ;	
	一维优化:f[j] = (f[j] + f[j - i]) % mod; 

#include //完全背包考虑 
using namespace std;
const int N = 1010,mod = 1e9 + 7;

int n;
int f[N];

int main()
{
	
	cin >> n;
	
	f[0] = 1;
	for(int i = 1;i <= n;i ++)
		for(int j = i;j <= n;j++)
			f[j] = (f[j] + f[j - i]) % mod;  //划分成不同价值
			
	
	cout << f[n] << endl;
	
	return 0;
}
动态规划
	状态表示  
		集合:所有总和是i , 并且恰好表示成j个数的和的方案
		属性:数量 
	状态计算
	分两类:每个数比较最小值是1 | 最小值大于1 
		 方案  f[i - 1][j - 1]代表和为 i - 1,有j-1个数 的方案数  , f[i - j][j] 和是i - j ,有j个数 的方案 
		 f[i][j] = f[i - 1][j - 1] + f[i - j][j];
		 ans = f[n][1] + f[n][2] + ... + f[n][n]; 
#include
using namespace std;
const int N = 1010,mod = 1e9 + 7;

int n;
int f[N][N];

int main()
{
	
	cin >> n;
	
	f[0][0] = 1;
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= i;j ++)
			f[i][j] = f[i - 1][j - 1] + f[i - j][j];
	
	int res = 0;
	for(int i = 1;i <= n;i++) res += f[n][i];
	
	cout << res << endl;
	
	return 0;
}



你可能感兴趣的:(蓝桥杯,算法,c++)