0
点赞
收藏
分享

微信扫一扫

蓝桥杯国赛十大必备技能——炎之呼吸.叁之型.动态规划

🔔燃烧心灵的,照亮动归的


往期精彩

🔖蓝桥杯十大常见天阶功法——水之呼吸.壹之型.递归
🔖蓝桥杯十大常见天阶功法——虫之呼吸.贰之型.二分
🔖 蓝桥杯十大常见天阶功法——音之呼吸.肆之型.模拟

炼狱先生

要紧牙关向前迈


💓关于动态规划的小阐述



不管黑猫白猫了,只要能Ac的,都是好喵喵~

在这里插入图片描述

听闫总讲课说,动态规划可以理解为对暴力的优化。
因为暴力是逐个逐个的处理每种情况,可想而知,其中存在大量的重复情况。动态规划就是将一些相似的情况化零为整了。
总的来说,就可以得到师兄总结的这张DP分析图了,感谢师兄🌹🌹🌹

在这里插入图片描述

大哥

💓壹 - 试题 算法提高 夺宝奇兵

线性动态规划模型——经典数字三角形的翻版

🌱题目描述

题目 1514: 蓝桥杯算法提高VIP-夺宝奇兵

题目 1514: 蓝桥杯算法提高VIP-夺宝奇兵

🌴解题报告

动归问题其实可以理解为是暴搜的优化,暴搜是把所有的状态,逐个逐个的进行处理,存在太多的重复的情况,但是动态规划则是将相同的情况归纳为一个集合。

记住这个一种线性DP的模型。它的背景是数字三角形,可能是大多数学动态规划的小伙伴的启蒙题目了。
数字三角形

将上述文字做成一张图,就是一张这种效果的闫式DP分析图:
数字三角形

因为,首先我们整体是一个归类的思想,所有从顶点走到图示中(4,2)这点的方案,算作一个集合。
无论是走的7->3->8->7还是的7->8->1->7,都是看做同一个类。

🌵参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;
const int N = 510,INF = 1e9;
int a[N][N];
int f[N][N];
int n;


int main()
{
    scanf("%d",&n);
    
    //输入
    for(int i = 1; i <= n;i++) 
        for(int j = 1; j <= i;j++) cin >> a[i][j];
        
    //初始化数组为负无穷,注意左边边界的空白和右边边界的空白也要初始化的
    //比如(2,2)位置的8,因为是集合的思想,它也可能从右上转移过来
    //但是那儿实际是没有数据的,因此处理为负无穷
    for(int i = 1; i <= n+1;i++)    
        for(int j = 0; j<=i+1;j++) f[i][j] = -INF;
    
    //开始DP
    f[1][1] =  a[1][1];
    for(int i = 2; i <= n;i++)  
        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;
}

💓贰 - 试题 历届试题 数字三角形【第十一届】【省赛】【C组】

经典线性DP——数字三角形进阶

🌱题目描述

试题 历届试题 数字三角形【第十一届】【省赛】【C组】

原题传送门

🌴解题报告

在这里插入图片描述

在这里插入图片描述

一个小小的脑筋急转弯

那么我就不重复的进行状态表示和状态计算的分析了,直接将DP分析图放上了。

数字三角形

🌵参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 110,INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];
int n;

int main()
{
	cin >> n;
	
	for(int i = 1; i <= n;i++)
		for(int j = 1; j <= i;j++) cin >> a[i][j];

	//初始化
	for(int i = 1; i <= n+1;i++)
		for(int j = 1; j <= i+1;j++)
		f[i][j] = -INF;

	f[1][1] = a[1][1];
	
	//开始DP
	for(int i = 2;i <= n;i++)
		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 ans = 0;
	//输入最后一排的个数是偶数,那么中间两个选
	if(n % 2 == 0) ans = max(f[n][n/2],f[n][n/2+1]);
	//最后一排是奇数,那么就输出中间位置上的数据
	else ans = f[n][n/2+1];

	cout << ans << endl;
	
	
	return 0;
}

💓叁 - 试题 历届真题 波动数列【第五届】【省赛】【A组】

组合类动态规划+掌握取模

🌱题目描述

题目描述
原题传送门

🌴解题报告

一、题意理解

其实是可以理解为枚举这n个数,用它们来构造数列。

那么,最后的提问,相当于问咱们,这种整数的个数有多少个。

二、样例演示

理解了题目表达的意思之后,着手对样例进行一下演示,一般可以从样例的演示从找到一些猫腻的喔~

l露出坏笑

经过整理,就可以得到如下式子:

s = n ∗ x + ( n − 1 ) ∗ d 1 + ( n − 2 ) ∗ d 2 + ( n − 3 ) ∗ d 3 + . . . + d n − 1 — — ① 式 s = n*x + (n-1)*d_1 + (n-2)*d_2 +(n-3)*d_3 + ... +d_{n-1} ——①式 s=nx+(n1)d1+(n2)d2+(n3)d3+...+dn1


咱们之前在题意理解中分析了,是 x x x经过运算得到一串数据。

x = s − ( ( n − 1 ) ∗ d 1 + ( n − 2 ) ∗ d 2 + ( n − 3 ) ∗ d 3 + . . . + d n − 1 ) n — — ② 式 \normalsize x = \frac{s-((n-1)*d_1 + (n-2)*d_2 +(n-3)*d_3 + ... +d_{n-1} )}{n} ——②式 x=ns((n1)d1+(n2)d2+(n3)d3+...+dn1)


三、综合分析

因为题目中说了 x x x整数

进而可以推导出来:

到这里了,答案离我们越来越近了。

拿捏了

四、闫式DP分析

最终目标:最后求得是 f [ n − 1 ] [ s f[n−1][s f[n1][s% n ] n] n]的值

波动数列

🌵参考代码(C++版本)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010,MOD = 100000007;
int n,s,a,b;
int f[N][N];//集合的意思是,只考虑前i项,且当前的总和除以n的余数是j的方案的集合的数量

//因为要涉及到取余这个操作,在C++中,负数取余得到的是负数,但是数组的索引下标不能为负
int get_mod(int a, int b)
{
    //假如a是-13,b是4。a除以b的正余数应该是1
    //a % b = -1。
    //a % b + b = 3
    //此时再取模得到的就是一个正余数了
    return (a % b + b) %b;
}

int main()
{
    cin >> n >> s >> a >>b;
    
    //初始化
    f[0][0] = 1;//也只有f[0][0]是有意义的,其他的是不合法的。
    
    //根据题意来,长度为n的序列:x,x+d1,x+d2,x+d3,…,x+dn−1
    //可以看出是d是从1执行到n-1
    for(int i =1; i < n;i++)
        for(int j = 0; j < n;j++)//余数的范围是从0~n-1
        f[i][j] = (f[i-1][get_mod(j- a * i,n)] + f[i-1][get_mod(j + b * i,n)]) % MOD;
    
    cout << f[n-1][get_mod(s,n)] << endl;
    return 0;
}

大致的了解到一些动态规划的形式之后,咱们来弄点综合一点的练习吧~
在这里插入图片描述

💓肆 - 试题 历届真题 地宫取宝【第五届】【省赛】【A\B\C组】

地图类型的——再听y总描述,待会调整描述的顺序+从数据范围推算法

🌱题目描述

试题 历届真题 地宫取宝【第五届】【省赛】【A\B\C组】
原题传送门

🌴解题报告

1、题意理解

我们可以很清晰的get到第一个限制:

知识的力量
继续读题,可以发现第二个限制

继续读题,瞬间就看到了第三个限制:

题目的最后,就是让我们在拿k件物品的情况下,小明有多少种走法。

重要技巧——从数据范围设计算法

剩余计算量
大约还有25次的计算量,这里应该是用于状态转移的计算了。那么整体框架,应该是五重循环。

2、梳理状态表示


3、状态转移分析

不取的情况比较容易分析,再分别加上数量 k k k以及手中最大价值 c c c,因为没有取,所有 k k k c c c都是不用变化的。即得:

以及

取的情况就稍微有点棘手了。

那么:

以及

结果统计就是将各个集合的个数全部加起来了。

将文字提炼一下,闫式DP分析图就可以拿捏出来了
闫式DP分析
通过这个图,我们可以看到,这回的DP分析是稍微有点复杂的了。

4、初始化

对于起点(1,1),咱们也是要进行取和不取两种状态的处理

倘若取:

倘若不取:

给坚持看到这儿的小伙伴擦擦泪水😭😭😭
在这里插入图片描述

🌵参考代码(C++版本)

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 55,MOD = 1000000007;
int n,m,k;
int w[N][N];//存放每个格子的宝贝的价值
int f[N][N][13][14];//数组或者说是集合f维护的是n行m列,在拿k个物品的情况下,且最后一件物品是c的状态的数量

int main()
{
    cin >> n >> m >> k;
    //录入信息
    for(int i = 1; i <= n;i++)
        for(int j = 1; j <= m;j++)
        {
            cin >> w[i][j];
            //这里有一步让w[i][j]都加1的操作,但是我没有太能get到
            w[i][j] ++;
        }
    
    //处理假如只有(1,1)这个位置
    //假如(1,1)这里拿了
    f[1][1][1][w[1][1]] = 1;
    //假如(1,1)这里不拿;
    f[1][1][0][0] = 1;
    
    //进入DP环节
    for(int i = 1; i <= n;i++)
        for(int j = 1; j <= m;j++)
        {
            if(i ==1 && j == 1) continue;
            for(int u = 0; u <= k;u++)
                for(int v = 0;v<=13;v++)
                {
                        int &val = f[i][j][u][v];
                        //处理不取的情况
                        //从上往下走,不取
                        val = (val + f[i-1][j][u][v]) % MOD;
                        //从左往右走,不取
                        val = (val + f[i][j-1][u][v]) % MOD;
                    if(u > 0 && v == w[i][j])
                    {
                        
                        //类似于求最长子序列一样,去枚举找到合适的
                        for(int c = 0; c < v;c++)
                        {
                            //从上往下走
                            val = (val + f[i-1][j][u-1][c]) % MOD;
                            val = (val + f[i][j-1][u-1][c]) % MOD;
                        }
                        
                    }
                }
        }
    
    int res = 0;
    for(int i = 0; i <= 13;i++) res = (res + f[n][m][k][i]) % MOD;
    cout << res << endl;
    
    return 0;
}

看我画了这么几张闫式DP分析图,会不会有小伙伴潜意识中逐渐以为,做动态规划一定要画闫式DP分析图了?我当初就是这种的想的,太呆萌了😭😭😭
在这里插入图片描述

还有就是有些题目的状态转移方程比较明显,或者友友们达到一定修为了,直接可以一眼将状态怎么转移的想清楚,那么此时就不用画图了wo~

举个不好听的例子吧,很多博客讲动态规划都是斐波那契数列跳台阶。我是真的不知道这两个直接可以三秒看出转移方程的题为什么要被这么多博主反复赘述

在这里插入图片描述

💓伍 - 试题 历届真题 调手表【第九届】【决赛】【B组】

一、记录是因为防止思维定势,闫式DP分析是帮助分析,不是说一定要画它。
二、它是有点类似于完全背包,但是和完全背包还是有点区别,比完全背包更单纯

🌱题目描述

历届真题 调手表【第九届】【决赛】【B组】
原题传送门1
原题传送门2

🌴解题报告

一、意识培养

二、温习完全背包

完全背包最淳朴的版本是这种的:

完全背包


对其进行状态表示和状态计算的分析:

状态表示:

状态计算:

完全背包

小伙们也可以看这张出上述步骤总结出来的闫式DP分析图,稍显凌乱了
完全背包

三、降服本题

闫式DP分析

状态表示:

这个题有点逗,它问题的意思是让咱们求一批最小调整方案中的最大值…离谱
在这里插入图片描述

状态计算:

要么从+1转移过来,要么从+k转移过来,那么还原回到(i-1)的状态的时候,只需要扣除相应的值,同时补上这次调整,也就是+1
写到这儿,我感觉它又像01背包了,这个题蛮可爱的。

f[i] = min(min(f[(i-1)%n], f[(i+n-k)%n]), f[i])+1;

最后遍历得到的方案,得到答案。

🌵参考代码(C++版本)

#include <iostream>
#include <algorithm>
#include <cstdio>

using namespace std;
long long n, k, j;
int f[100010];
int main()
{

  cin >> n >> k;
  //初始化,默认一次是一分钟,那么现在是多少次,就是多少分钟
  for(int i = 0; i < n; i++)  f[i] = i;

   //进行一步预处理,找到每个时间点,一次走k下,所有需要的最少次数
  for(int i = 1; i < n; i++)
  {
    j = i*k % n;
    f[j] = min(f[j], i);
  }
  
  //因为f[0]不用处理了,从2开始枚举
  for(int i = 2; i < n; i++)  f[i] = min(min(f[(i-1)%n], f[(i+n-k)%n]), f[i])+1;
  
  int maxv = 0;
  
  for(int i = 1; i < n; i++)  maxv = max(maxv, f[i]);
  
  cout << maxv << endl;
  
  return 0;
}
既然掌握了完全背包,来看看蓝桥中也是比较常见的01背包吧。

💓陆 - 试题 历届真题 砝码称重【第十二届】【省赛】【A/B/C组】

变型的01背包

🌱题目描述

试题 历届真题 砝码称重【第十二届】【省赛】【A/B/C组】

原题传送门

🌴解题报告

看到题目中说到让咱们寻找一个最优解,那么这个时候,可以尝试向着动态规划的方向去思考。

系统温习背包问题:

对于本题:

状态表示:
集合:集合f[i][j]表示的含义是从前i个砝码中进行选择且总体积为j的所有方案的集合

属性:这个集合所存储的属性是集合是否非空。表示从前i个砝码中选出总重量为j的方案是否存在,可以很明显的看出,是一个bool值。

状态计算:

状态计算对应的是对咱们规定的集合的划分,划分的依据大多是最后一个不同点。

比如本题,最后一个不同点就是当前这个砝码是怎么进行放置:

将如上的步骤进行整理的,那么就可以得到这张闫式DP分析图
砝码称重
同样的,对应一定会选择的砝码i,依旧先采用先剖除它并不会影响整体格局的思想,也就是i-1,对应修改总和j受到的影响。

🌵参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110,M = 100010;
int n,m;
int w[N];//存储砝码质量的数组
bool f[N][2 * M];//表示状态的数组


int main()
{
    cin  >> n;
	//DP环节会有减一的,都建议从1开始获取值
    for(int i = 1; i <= n;i++) cin >> w[i],m += w[i];
    
    //初始化,这行代码的意思是,从前0个砝码中选出总和为0的方案是存在的
	//M 只是都要加的一个偏移量
    f[0][M] = true;
	//枚举n个砝码
    for(int i =1; i <= n;i++)
    {
    	//枚举会出现的重量,因为可能为负,所有统计加上了一个偏移量M
    	for(int j = -m; j <= 2 * m ;j++)
    	{
			//情况一:不选当前砝码的集合不是空
			if(f[i-1][j+M])
			{
				f[i][j+M] = true;
				continue;
			}
			if(j - w[i] >= -m &&f[i-1][j- w[i] + M])
			{
				f[i][j+M] = true;
				continue;
			}
			if(j + w[i] <= m && f[i-1][j+w[i] + M])
			{
				f[i][j+M] = true;
			}
		}
	}
	
	//遍历得到的方案,统计结果
	unsigned long long cnt = 0;//unsigned可用于防止溢出
	for(int i = 1; i <= m;i++)
		if(f[n][i+M]) cnt ++;

	cout << cnt << endl;
//	printf("%lu",cnt);
	return 0;
}
再系统的看一道背包问题,作为道别吧,蓝桥杯中完全背包和01背包出现得还是挺勤的

💓柒 - 试题 历届真题 包子凑数【第八届】【省赛】【A/B组】

纯正的完全背包+互质数学规律

🌱题目描述

试题 历届真题 包子凑数【第八届】【省赛】【A组】

原题传送门

🌴解题报告

感觉这个题,读起来有点拗口,不知道小伙伴们有没有这种感觉了,

通俗来说了,就是包子大叔有很多笼固定数量的包子,比如5个一笼的,9个一笼的。
有无限笼,注意无限这两个字,然后细细品读这句话小明想知道一共有多少种数目是包子大叔凑不出来的。

这句话的意思就是让咱们找到最优解,那么动态规划可以拿出来了,看到无限二字,向着完全背包的方向思考。

对于完全背包状态转移方程通式的获得是经过如下的DP分析以及数学归纳之后的结果,我只是放置在这儿,根据小伙伴们的需求,能理解性的记忆最好。
假如时间吃紧的话也可以直接背下来先用着,以后再回头理解它的时候回会很轻松的

完全背包

本题需要注意的第二个点是一个数学规律:

整数间若不互质,最大公因子为d,即每个整数都是d的倍数。

d > 1时,最大不能组成的整数为无穷大。因此,可以先计算出n个整数的最大公因子,若 > 1,则输出INF。
若等于1,则采用动态规划进行分析

状态表示:

状态计算:
依旧是依据最后一个不同点,完全背包中,最后一个不同点就是当前这个物品i选择的数量。可以选0个,也可以选k个。

🌵参考代码(C++版本)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 110, M = 10010;//M 是 最多一百种蒸笼,每笼包子最多100个
int v[N];
bool f[N][M];
int n;

//判断最大公约数,即欧几里得算法
int gcd(int a,int b)
{
	return b? gcd(b,a % b) : a;
}

int main()
{
	//处理输入
	cin >> n;
	for(int i  =1; i <= n;i++) cin >> v[i];
	
	//根据本题的数学背景。现在要去找到读入的数据的最大公约数
	
	int x = v[1];
	for(int i = 2; i <= n;i++) x = gcd(x, v[i]);
	
	//判断最大公约数是否大于1
	if(x > 1) printf("INF\n");
	
	//最大公约数为1,也就说,它们是互质的,那么开始DP
	else
	{
		//初始化
		f[0][0] = true;
		
		for(int i = 1; i <= n;i++)
			for(int j = 0; j <= M;j++)
			{
				if(f[i-1][j])
				{
					f[i][j]  =true;
					continue;
				}
				int k = 1;
				while(j - k * v[i] >= 0)
				{
					if(f[i-1][j-k*v[i]])
					{
						f[i][j] = true;
						break;
					}
					//跳出当前的选择,进行一下个
					k++;
				}
				
			}
		long long cnt = 0;
		//遍历所有组合,输出答案
		for(int i = 1; i <= M;i++)
			if(f[n][i] == false) cnt++;

		cout << cnt <<endl;
	}
	return 0;
}

到这儿了,DP中比较简单的内容算是告一段落,就像炭炭和猪猪合手解决上弦之一的梦魇。要着手迎战三哥了

在这里插入图片描述

💓捌 - 试题 历届真题 二进制问题【第十二届】【决赛】【B组】

数位DP——有一定的模板

🌱题目描述

二进制问题

原题传送门

🌴解题报告

我大致看了三个数位DP的题,感觉它对比起其他DP,相对是友好一点的,因为它可以理解为是有一个浅浅的模板的。

数位DP我看的是这位大佬的文章数位dp总结 之 从入门到模板。但是感觉大佬的排版稍微有点凌乱,我后面再刷几个数位DP的题了,结合大佬的文章,做一期数位DP的博客。

🌵参考代码(C++版本)

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 70;
int bits[N];
LL f[N][N][2];
LL n, k;

LL dfs(int pos, LL count, int limit)
{
	//已经将现有的位数递归完,结束递归
    if (pos == -1) return count == k;
    
    if (!limit && f[pos][count][limit] != -1) return f[pos][count][limit];
    
    //确定可以枚举的上界
    int up = limit ? bits[pos] : 1;
    
    LL ans = 0;
	//在上界范围内枚举
    for (int i = 0; i <= up; ++i) ans += dfs(pos - 1, count + (i == 1), i == up && limit);

    if (!limit) f[pos][count][limit] = ans;
    
    return ans;
}

int main()
{
    cin >> n >> k;
    int len = 0;
    memset(f, -1, sizeof(f));

    while (n)
	{
        bits[len ++] = n & 1;
        n >>= 1;
    }
	
    cout << dfs(len - 1,0, 1) << endl;
    return 0;
}
数位DP我暂时还没有完全get到它这个模板的玩法,就稍显粗糙了。

在这里插入图片描述

下面着手去解决蓝桥中更为常见的最长公共子序列和最长上升子序列了

💓玖 - 试题 历届真题 密码脱落【第七届】【省赛】【A组】

最长公共子序列(LCS)、区间DP

🌱题目描述

密码脱落

原题传送门

🌴解题报告

一、逆向思维

结合样例演示:

二、闫式DP分析

状态表示——问题问什么,咱们就表示什么

集合:f[i,j]表示从i到j这个区间中,回文子序列的集合

属性:最大值

状态计算:

小伙伴们应该可以发现,情况四其实是有部分和情况二和情况三重合了。那咱们现在只需要处理三种状态了。去每种状态中找到最大值。

🌵参考代码(C++版本)

#include <iostream>
#include <cstring>
#include <algorithm>
#include <string.h>

using namespace std;
const int N = 1010;
int f[N][N];
char s[N];

int main()
{
    //输入
    scanf("%s",s);
    int n = strlen(s);
    
    //小小的模板了:枚举当前这个区间
    for(int len = 1; len <= n;len ++)
        //枚举左端点
        for(int i = 0; i + len - 1 < n;i++)
        {
            //枚举右端点
            int j = i + len -1;
            if(len == 1) f[i][j] = 1;
            else 
            {
                //只有左端点、只有右端点、以及左右端点都没有的情况
                f[i][j] = max(f[i+1][j],f[i][j-1]);
                //两个端点都有
                if(s[i] == s[j]) f[i][j] = max(f[i][j],f[i+1][j-1]+2);
            }
        }
    
    printf("%d\n",n - f[0][n-1]);
    return 0;
}
才接住了三哥的一招空式,现在着手接碎式吧!~

在这里插入图片描述

💓拾 - 试题 历届真题 游园安排【第十一届】【决赛】【A/B组】

最长上升子序列(LIS)

🌱题目描述

试题 历届真题 游园安排【第十一届】【决赛】【A/B组】

原题传送门1
原题传送门2

🌴解题报告

这个题直接用 O ( N 2 ) O(N^2) O(N2)的时间复杂度的动态规划来做是会因为超时,最终导致只能得到70分。

我直接把代码放这儿吧,可以作为一个练习的参考,要Ac咱就不用它了。

最长子序列的算法板子我是在acwing上学的,就把闫总的题目放置在这儿了

朴素版本 时间复杂度 O ( n 2 ) O(n^2) O(n2)AcWing 895. 最长上升子序列
贪心 + 二分 时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)AcWing 896. 最长上升子序列 II

🌵参考代码1——dp 70分(C++版本)

参考代码1——dp 70分
就当温习一下朴素版本LIS的模板吧

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>

using namespace std;
const int N  = 1e6 + 10;
string s;
int f[N];//存储集合的数组
string v[N];//存储人名的数组

int main()
{
	//输入
	cin >> s;
	
	int num = 0;
	//将输入的字符串依据大写,分割为人名存储到数组中
	for(int i = 0; i < s.length();i++)
		if(s[i] >= 'A' && s[i] <= 'Z')//当目前这字符是大写的时候
		{
			num ++;
			v[num] = "";//每次用一个空字符串来分割人名
			v[num] += s[i];
		}else
		{
			v[num] += s[i];//意思当前不是大写,不必进行人名的分割,直接接在现有的字符串数组后面就好
		}

	int max_len = 0;//统计最长上升子序列的个数,初始化为0
	
	int pos = 1;//记录位置
	//下面就是标准的最长上升子序列的模板了...
	for(int i = 1; i <= num;i++)
	{
		f[i] = 1;
		//查找人名
		for(int j = 1; j < i;j++)
			if(v[j] < v[i]) //如果上一个人名小于第j个人名
				f[i] = max(f[i],f[j]+1);

		if(f[i] >= max_len) max_len = f[i],pos = i;
	}
	
	//这个题,假如在不超时的情况下,就是这个路径输出比较棘手
	string res;
	for(int i = pos; i >= 1;i--) 
	{
		printf("if外的f[%d]是%d\n",i,f[i]);
		if(f[i] == max_len) //利用自己统计出来的最长长度来输出路径
		{
			res = v[i] + res;
			cout <<"当前的res = "<< res << endl;
			max_len --;
		}
	}
	
	cout << res << endl;
		return 0;
}

下面着重说一下怎么贪心 + 二分解决这个题。

总的来说,遇到大的就尾部插入,遇到小的就内部替换,最终的到的就是一个严格上升的序列。

🌵参考代码2——贪心 + 二分 100分(C++版本)

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <string.h>
#include <vector>

using namespace std;
const int N = 1e6 + 10;
string name[N];//存储人名的数组
string s;
vector<int> f;//f 存储的是每个子序列的最长上升子序列的长度
vector<string> str;

int main()
{
	//输入
	cin >> s;

	//老规格,把字符串按照题目要求分割开
	int num = 0;
	for(int i = 0; i < s.length();i++)
	{
		if(s[i] >= 'A' && s[i] <= 'Z')
		{
			if(i!=0) num ++;
			name[num]  = "";
			name[num] += s[i];
		}
		else name[num] += s[i];
	}

	str.push_back(name[0]);
	f.push_back(1);
	//遍历分割到的名字
	for(int i = 1; i <= num;i++)
	{
		//如果现在这个i所在的串是比已经存储的名字串的最后一个还大
		if(name[i] > str.back())
		{
			str.push_back(name[i]);//加入如当前的串
			//更新当前动态数组的长度
			f.push_back(str.size());
		}
		else
		{
			//当前这个i所在的串是比已经存储的名字串的最后一个小
			//进入贪心环节
			//利用二分找到大于等于序列name[i]的第一个元素的位置

			int pos = lower_bound(str.begin(),str.end(),name[i]) - str.begin();

//			将这个位置的元素替换为v[i]
            str[pos] = name[i];
            //更新当前动态数组的长度
            f.push_back(pos+1);

		}
	}

	string ans[N];//统计结果
  	int cnt = 0;//计数器
	for(int i = num,j = str.size(); j > 0; i--)
	{
		if(f[i] == j)
		{
			ans[cnt ++] = name[i];
			j --;
		}
	}
	//输出获得的姓名串
	for(int i = cnt -1; i >= 0; i--) cout << ans[i];

    return 0;

}

在这里插入图片描述

抽刀,再战~

💓拾壹 - 试题 历届真题 生命之树【第六届】【省赛】【B组】

树形DP

🌱题目描述

试题 历届真题 生命之树【第六届】【省赛】【B组】
原题传送门

🌴解题报告

树形DP了,本质是不难的,只是假如没有提前接触。不太能想到那个点上去。

一、背景剖析

斜二叉树
同样的,线性DP是把DP在一段类似于链表的、线性的结构(比如数组)上进行。树形DP只是把背景搬到树上来了。

二、解决思路

三、闫式DP分析

状态表示:

这个题,感觉状态转移方程都没有了…

最大值

🌵参考代码(C++版本)

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010,M = 2*N;

typedef long long LL;

int n;
int w[N];//树结点的权重
int h[N],e[M],ne[M],idx;//邻接表
LL f[N];//状态数组


//默写使用邻接表构建边的函数
void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}


//存一下当前搜的父结点是谁,防止往回搜
void dfs(int u, int father)
{
    f[u] = w[u];
    
    //搜一下这个u的儿子
    for(int i = h[u];i != -1;i = ne[i])
    {
        int j = e[i];
        //如果不是向回搜的
        if(j != father)
        {
            //递归进去,并标记当前这个结点是从u下来的
            dfs(j,u);
            f[u] += max(0ll,f[j]);
        }
    }
    
}

int main()
{
    
    scanf("%d",&n);
    memset(h,-1,sizeof h);
    //读入每个 点的权值
    for(int i = 1; i <= n;i++) scanf("%d",&w[i]);
    
    //读入每个边的情况
    for(int i = 0; i < n-1;i++)
    {
        int a,b;
        scanf("%d %d",&a,&b);
        add(a,b),add(b,a);
    }
    
    //递归搜索
    dfs(1,-1);
    
    //求最大值
    LL res = f[1];
    for(int i = 2;i <= n;i++) res = max(res,f[i]);
    
    if(res < 0) res = 0;
    printf("%lld\n",res);
    
    return 0;
    
}
终是大哥以凡人之躯比肩神明,我以不惧失败之势鏖战动归

在这里插入图片描述

💓奥义 - - 第十二届蓝桥杯 C/C++A组 回路计数

状态压缩DP

🌱题目描述

第十二届蓝桥杯 C/C++A组 回路计数

原题传送门

🌴解题报告

哈密尔顿回路

在这里插入图片描述

举个直白的例子:当前的状态i的二进制表示为(1101101),对于当前状态而言,是1的位置就表示走过了,是0的位置就表示没有走过。

友友们可以脑补一下,对于当前的状态i,就是第一栋教学楼走了,第二栋没有走,第三栋走了,第四栋走了…

状态表示:
集合:所有从点1走到点j,经过的点的状态为i的集合
题目要统计的是数量,那么
属性:数量Count

状态计算:

综上所述,就是得到了这个可爱的闫式DP 分析图:
闫式DP分析

🌵参考代码(C++版本)

#include <iostream>
#include <algorithm>

using namespace std;
typedef long long LL;
const int N = 22,M = 1 << N;
bool w[N][N];//记录i是否能走到j
LL f[M][N];//所有从点1走到点j,经过的点的状态为i的集合


//判断是否互质的欧几里得算法gcd
int gcd(int a,int b)
{
	return b ? gcd(b,a%b):a;
}

int main()
{
	//处理这21座教学楼的连通情况
	for(int i = 1; i <= 21;i++)
		for(int j = 1; j <= 21;j++)
			if(gcd(i,j) == 1) w[i][j] = true;
	//初始化,
	f[2][1] = 1;
	//枚举这2的21次方种状态
	//f[i][j]:从 1 走到 j,且经过的点的状态为 i 的方案数
	//做了一下步优化,可以少进行两次循环了
	for(int i = 2; i <= M-2;i++)
		for(int j = 1;j <= 21; j++)
		{
			//如果到j这栋教学楼的路径,也是属于到i这栋教学楼的一部分
			if((i >> j) & 1)
			{
				//枚举当前的j是从1~21中哪栋楼转移过来的
				for(int k = 1; k <= 21;k++)
				//记住w[k][j]很重要,因为互质,导致两栋楼是联通的,这种才能实现从k再到j
					if(i - (1 <<j) >> k & 1 && w[k][j])
					//i - (1 << j)表示从总状态i中扣除从1到j的状态
					f[i][j] += f[i - (1 << j)][k];
			}
		}

	LL ans = 0;
	//遍历这些集合,统计结果
	for(int i = 2; i <= 21;i++)
		ans += f[M-2][i];

	cout << ans << endl;
	return 0;
}

💓 总结

这种图是从蓝桥官方给的近三年知识点总结的pdf中截取的。
在这里插入图片描述

我这篇叁之型差不多将蓝桥中容易出现的动态规划都系统的总结了一遍了。但是可能还是不够全面的,感觉动态规划算是我算法生涯中的三哥吧,无限列车和炼狱第一次邂逅它,苦战。后面在无限城中再遇三哥的时候,我希望我再写动态规划,会更成熟、更精妙,彻底战胜三哥猗窝座吧~

💓 关于备战的一点建议

可能有小伙伴说,有些题解博客好糟糕,看着太晦涩了又找不到问题的地方。友友你可以私信问我,但是我因为这个学期加了课程,就挺吃力的。有时候因为我自己能力不够或者我直接被学校里的事儿耽搁了不能及时回复😭😭😭,所以更倾向大家请教这几位博主喔,他们都是我非常钦佩的大佬,私信他们就好喔~

在这里插入图片描述
C/C++

Pluto(算法健将,各类周赛打宝宝级别)个人主页
泡泡(以大一学籍让无数大四学长自愧不如,正在刷爆洛谷)个人主页
折叠的小饼干(实力派学姐,温柔耐心~)个人主页
knao_s(题解绝绝子,除了详细到位,还是详细到位)个人主页
永遇乐金枪鱼(一位谦虚的大佬,精准把握你题解思路中不对的地方个人主页

Java

执梗(带三百人冲刺蓝桥主要负责人,讲题细心,出题尽职尽责🌹)个人主页
小怂(用最朴素的for、while、if刷爆蓝桥云课,担心不熟悉数据结构影响解题的小伙伴可以多请教他喔~)个人主页
小羊不会飞(题解常年稳居热榜前八,题解质量经得起检验,为人谦虚耐心)个人主页
Hydrion-Qlz(西安交大大佬,算法爱好者)个人主页
小成同学(acwing师兄,刷题健将,考虑问题周全)个人主页
托马斯—酷涛(Java算法爱好者,对多个方向均有涉猎)个人主页
小王同学(算法博客详细,答疑热情,登上热榜第一个人主页

Python

小郑(国赛强劲实力选手,热榜常客,求知欲很强)个人主页
小蓝(算法思路清晰,博客题解详细)个人主页
秋刀鱼(会三门语言,题解地表最详细)个人主页
文章很长,因为想让大家真的学会动态规划呀~,而不是只能看到一个斐波那契数列和爬楼梯的例题演示。谢谢观看呀🌹🌹🌹
无限列车意难平,那么准备到无限城中看善逸清理门户叭(柒之型的搜索完成一半啦)

在这里插入图片描述

举报

相关推荐

0 条评论