DP汇总

本文详细介绍了动态规划的几种类型,包括线性DP、区间DP、状态压缩DP、树形DP和记忆化搜索。通过具体的编程示例,如最长上升子序列、最短编辑距离、最长公共子序列等,阐述了如何运用动态规划解决这些问题,并提供了高效算法的实现。
摘要由CSDN通过智能技术生成

目录

一,线性DP

1,数字三角形

2,最长上升子序列

①,朴素解法

②,用二分优化的解法 

3,最长公共子序列

4,最短编辑距离

二,区间DP

1,石子合并

三,状态压缩DP

1,AcWing 291. 蒙德里安的梦想 

 2,AcWing 91. 最短Hamilton路径

四,树形DP

AcWing 285. 没有上司的舞会

五,记忆化搜索

AcWing  901.滑雪

六,数位统计DP

AcWing 338. 计数问题


一,线性DP

1,数字三角形

题目描述

示出了一个数字三角形。  请编一个程序计算从顶至底的某处的一条路 径,使该路径所经过的数字的总和最大。  每一步可沿左斜线向下或右斜线向下走;  1< 三角形行数< 25;  三角形中的数字为整数< 1000;

输入格式

第一行为N,表示有N行 后面N行表示三角形每条路的路径权

输出格式

路径所经过的数字的总和最大的答案

输入样例 

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

输出样例 

30

 用一个二维数组dp[i][j] 表示从顶点走到第 i 行第 j 列的值的集合,集合属性为最大值,

可以发现dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])

题目可能出现某个位置上是负数的情况,所以首先要初始化为负无穷

代码如下:时间复杂度O(N^2)

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 30,INF=0x3f3f3f3f;

int n;
int dp[N][N], a[N][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 = 0; i <= n; i++)
        for (int j = 0; j <= i + 1; j++)
            dp[i][j] = -INF;
    dp[1][1] = a[1][1];
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i; j++)
            dp[i][j] = max(dp[i - 1][j] + a[i][j], dp[i - 1][j - 1] + a[i][j]);
    int max1 = 0;
    for (int j = 1; j <= n; j++)
        max1 = max(max1, dp[n][j]);
    cout << max1;
    return 0;
}

2,最长上升子序列

题目描述

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

输入样例 

7
3 1 2 1 8 5 6

输出样例 

4

数据范围与提示

1 ≤ N ≤ 1000,
−109 ≤ 数列中的数 ≤ 109

①,朴素解法

时间复杂度为O(N^2)

 以一个一维数据dp[i]表示以 结尾的最长上升子序列的集合,集合属性为最大值

如果i的前一个数j小于i,那么dp[i]=max(dp[j]+1)

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;


const int N=1e4;

int a[N],dp[N];
int n;
int main()
{
    int ans=0;
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        dp[i]=1;
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])
                dp[i]=max(dp[i],dp[j]+1);
        }
        ans=max(ans,dp[i]);
    }
    cout<<ans;
    return 0;
}

②,用二分优化的解法 

优化后的时间复杂度为O(Nlog N)

对于每个数 i ,我们只要找到在他前面的的数中以比他小的数中的最大的那个数结尾的上升序列长度,在他后面加1就行了,所以我们只要记录对于每一个长度的上升序列的以最小值结尾的那个数,比如数5和数8的上升子序列长度都是3,那么只用将5记录就可以了,因为对于后面的数,如果大于8的话一定大于5,这样就能保证求得的以每个数结尾的上升子序列的长度是最大的

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n;
//len记录最长上升子序列的值
//q记录以长度为N的上升子序列结尾的最小值
int q[N], a[N],len;
int main()
{
	cin >> n;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	for (int i = 0; i < n; i++)
	{
		int l = 0, r = len;
		while (l < r)
		{
			int mid = l + r + 1 >> 1;
			//找到以a[i]结尾的上升子序列的长度最大值
			if (q[mid] < a[i])
				l = mid;
			else
				r = mid - 1;
		}
		len = max(len, r + 1);
		q[r + 1] = a[i];
	}
	cout << len;
	return 0;
}

3,最长公共子序列

题目描述

给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数 N 和 M。

第二行包含一个长度为 N 的字符串,表示字符串 A。

第三行包含一个长度为 M 的字符串,表示字符串 B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

输入样例 

4 5
acbd
abedc

输出样例 

3

数据范围与提示

1 ≤ N, M ≤ 1000

 用一个二维数组dp[i][j]表示所有在第一个序列的前i 个字母中出现,且在第二个序列的前 j 个字母出现的子序列的集合,集合属性为最大值

我们将dp[i][j]进行集合划分

假设dp[i][j]不是以第i个字母和第j个字母结尾的,那就是dp[i][j]=dp[i-1][j-1]  ①

假设dp[i][j]是以第j个字母结尾,那就是dp[i][j]=dp[i-1][j] ②

假设dp[i][j]是以第i个字母结尾,那就是dp[i][j]=dp[i][j-1] ③

假设dp[i][j]是以第i个字母和第j个字母结尾的,那就是dp[i][j]=dp[i-1][j-1]+1 ④ 

但是这里要注意,②和③中,两个等式其实并不等价,但是结合我们对dp集合的定义可以发现等式后面的情况会包含前面的情况,以及包含第①种情况,所以两种情况之间是会有重复的,但是我们求得是集合得最大值,所以最终的答案不影响

代码如下:时间复杂度O(NM)

#include<iostream>
#include<algorithm>
using namespace std;

int main()
{
    int n,m;
    int f[1001][1001];
    char a[1001],b[1001];
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= m; i ++) cin >> b[i];
    
    for(int i = 1; i <= n; i ++)
    {
        for(int j = 1; j <= m; j ++)
        {
            
            if(a[i] == b[j]) 
                f[i][j] = f[i - 1][j - 1] + 1;
            else
                f[i][j]=max(f[i-1][j],f[i][j-1]);
        }
    }
    cout << f[n][m];
}


4,最短编辑距离

题目描述

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

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。
 

输入格式

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

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

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

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

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

输出格式

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

输入样例

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例 

4

数据范围与提示

1 ≤ n, m ≤ 1000

用一个二维数组dp[i][j]表示所有将a[1~i]变成b[i~j]的操作方式的集合,集合的属性为最小值

dp[i][j]可以由增删改三种情况转移过来

对于增,我们看a的最后一个元素,要想将a增加一个元素使得a变成b,那么一定要在a[1~i]与b[1~j-1]配对成功的基础上加上一步操作,即dp[i][j-1]+1

对于删,我们要删除a的最后一个元素使得a变成b,那么一定要先满足a[1~i-1]与b[1~j]配对成功的基础上加上一部操作,即dp[i-1][j]+1

对于改,由两种情况,如果a的最后一个元素等于b的最后一个元素,那么dp[i][j]=dp[i-1][j-1]

如果不相等的话,那么就是dp[i-1][j-1]+1

dp[i][j]就是上述转移状态的最小值

代码如下:

一定要注意要初始化,前面都没有初始化是因为当状态都为0时的方案数都是0,这里不一样,因为状态转移涉及了i-1,所以下标从1开始遍历比较方便,同时我们要初始化下标为0的情况,分别表示a数组为0时变成b的操作步数和b数组为0时,a变成b的操作步数

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

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

int main()
{
	cin >> n >> a + 1;
	cin >> m >> b + 1;
	//注意要初始化
	for (int i = 0; i <= m; i++)dp[0][i] = i;//a数组为空时变成b的操作步数
	for (int i = 0; i <= n; i++)dp[i][0] = i;//b数组为空时a变成b的操作步数

	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
		{
			dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
			if (a[i] == b[j])
				dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] );
			else
				dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + 1);
		}
	}
	cout << dp[n][m];
	return 0;
}

二,区间DP

1,石子合并

题目描述

设有 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)。

输出格式

输出一个整数,表示最小代价。

输入样例

4
1 3 5 2

输出样例 

22

数据范围与提示

1 ≤ N ≤ 300

 对于这类题,我们就要用到区间dp

用一个二维数组dp[i][j]表示从第i堆到第j 堆的合并方式的集合,集合属性为最小值

以最后一次合并为分界线,即将dp[i][j]分成dp[i][k]和dp[k+1][j],k=i,i+1,i+2……j-1,最后再将这两堆合并,也就是在加上第i堆到第j堆的石子重量,dp[i][j]就是全部情况的最小值,即

dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]) ,k=i,i+1,i+2……j-1。

代码如下:

对于区间dp,首先要枚举区间,求出了小区间的最小值才能求出大区间的最小值

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 310;

int f[N][N], s[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> s[i];
	for (int i = 1; i <= n; i++)s[i] += s[i - 1];//求前缀和,方便后面直接加上区间石子的总重量
	//len表示区间,当区间为1时,表示只有一堆石子,合并体力为0,所以从2开始
	for (int len = 2; len <= n; len++)
	{
		for (int i = 1; i + len - 1 <= n; i++)
		{
			int l = i, r = i + len - 1;
			f[l][r] = 1e9;//一定要记得初始化,否则最小值永远是0
			for (int k = l; k < r;k++)
			{
				f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
			}
		}
	}
	cout << f[1][n];
	return 0;
}

三,状态压缩DP

1,AcWing 291. 蒙德里安的梦想 

题目描述

求把 N×M的棋盘分割成若干个 1×21×2 的长方形,有多少种方案。

例如当 N=2,M=4时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N和 M。

当输入用例 N=0,M=0时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1≤N,M≤11

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205

 首先我们分析一下这个问题,每个小方块可以横着摆,也可以竖着摆,如果我们把横着摆的方案全部摆完,接着就只用把竖着的小方块插空就行了,所以我们只用考虑横着的小方块摆放的所有方案数。

我们以dp[i][j]表示前 i-1 列已经摆好,且从第 i-1 列伸到第 i 列的状态为 j 的一个方案数,用一个二进制数表示,伸出来的用1表示,没伸出来的用0 表示,例如在一个5*5的方格中第 i-1 列的第一,三,五行伸到第 i 列的状态 j 就用一个五位二进制数10101来表示,几行就用几位二进制数表示

从第 i-1 列伸出到第 i 列需要满足两个条件,第 i-1 列状态k与第 i 列的状态 j 不能冲突,例如第 i-1列的状态k为10110的话,表示第 i-2  列的第一,三,四行伸到了第 i-1 列,那么就说明第 i -2 列到第 i-1 列的第一,三,四行摆放 了一个1*2的小方块,那么第i-1行的第一,三,四行就不可能伸到第i列的第一,三,四行了,不然就是一个1*3的小方块了,所以可以用 j&k必须为0,同时,对于每一列的状态,不能有奇数个连续的0,否则1*2的小方块会插不进来,即 j | k不能有奇数个连续的0,对于这一步我们可以预处理出来,对于 j | k 这里解释以下为什么看第 i - 1 列得状态 k 是否满足要求要或上 i  -  1 后面那一列i的状态 j ,因为j表示的是 第 i  - 1 列伸出来到第 i 列的状态,他们两个是相互关联的,如果某一行伸到了第i列,表示第 i  -  1 列这一行也有小方块,就不能放竖直的小方块进去了

代码如下:

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

using namespace std;

const int N = 12;

int n, m;
//第一维表示列,第二维表示状态
int dp[N][1 << N];
//表示某个状态是否满足要求
bool flag[1 << N];

int main()
{
	while (cin >> n >> m, n || m)
	{
		//因为有多组测试数组,所以每次要初始化
		memset(dp, 0, sizeof dp);
		//预处理出来n位二进制的数是否满足要求,不存在奇数个连续的0
		for (int i = 0; i < 1 << n; i++)
		{
			flag[i] = true;
			int cnt = 0;
			for (int j = 0; j < n; j++)
			{
				if (i>>j & 1)
				{
					if(cnt&1)
						flag[i] = false;
					cnt = 0;
				}
				else
					cnt++;
			}
			if (cnt & 1)
				flag[i] = false;
		}
		//dp开始
		dp[0][0] = 1;
		//遍历每一列,找到这一列满足条件的状态数
		for (int i = 1; i <= m; i++)
		{
			for (int j = 0; j < 1 << n; j++)
			{
				//找到i-1列满足条件的状态数
				for (int k = 0; k < 1 << n; k++)
				{
					if ((j & k) == 0 && flag[j | k])
						dp[i][j] += dp[i - 1][k];
				}
			}
		}
		//根据定义可知dp[m][0]为前m-1列都已经摆好,且没有伸到第m列得方案数
		cout << dp[m][0] << endl;
	}
	return 0;
}

 2,AcWing 91. 最短Hamilton路径

题目描述

给定一张 n个点的带权无向图,点从 0∼n−1标号,求起点 0到终点 n−1的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 n。

接下来 n 行每行 n个整数,其中第 i行第 j 个整数表示点 i到 j 的距离(记为 a[i,j]])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x],并且 a[x,y]+a[y,z]≥a[x,z]

输出格式

输出一个整数,表示最短 Hamilton 路径的长度。

数据范围

1≤n≤20
0≤a[i,j]≤10^7

输入样例:

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:

18

用一个二维数据dp[i][j]来表示从0号点走到 j 号点经过的状态是 i 的所有路径,集合属性为最小值,i 的二进制表示走过了哪些点,例如10100就是走了2,4号点,假设走到 j 号点前先走到了 k 号点,那么dp[i][j]就等于从0号点走到 k 号点的路径的最小值加上从k号点走到 j号点的距离,那么就是dp[i][j]=dp[i^(1<<j)][k]+w[k][j],这里解释一下i^(1<<j),因为题目要求每个点只能走一次,所以要想从 k 号点走到 j 号点,那么在0号点走到 k 号点一定没有走过 j 号点

代码如下:

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

using namespace std;

const int N = 20;

int n;
int w[N][N];//表示路径的边权值
int dp[1 << N][N];//一维用二进制的形式表示路径,第二维表示从0号点走到N号点

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> w[i][j];
    memset(dp, 0x3f, sizeof dp);//首先要初始化为最大值
    dp[1][0] = 0;//初始化从0号点走到0号点经过0号点的距离为0
    for (int i = 0; i < 1 << N; i++)//遍历所有路径
    {
        for (int j = 0; j < n; j++)//遍历所有以j号点结尾的点
        {
            if (i >> j & 1)//如果路径中包含了经过j号点的路径才能进行下一步的状态转移
            {
                for (int k = 0; k < n; k++)//遍历中转点,表示经历k号点走到j号点
                {
                    if ((i ^ (1 << j)) >> k)//如果路径中包括了走到k号点,才能用k号点进行中转
                        dp[i][j] = min(dp[i][j], dp[(i ^ (1 << j))][k] + w[k][j]);//更新答案
                }
            }
        }
    }
    //根据定义可以知道题目要求的是从0号点走到n-1号点经过每个点的路径
    //用(1<<n)-1就可以得到一个全1的序列,也就是经过每个点的路径
    cout << dp[(1 << n) - 1][n - 1];
    return 0;
}

四,树形DP

AcWing 285. 没有上司的舞会

题目描述

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式

第一行一个整数 N。

接下来 N行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K是 L 的直接上司。(注意一下,后一个数是前一个数的父节点,不要搞反)。

输出格式

输出最大的快乐指数。

数据范围

1≤N≤6000,
−128≤Hi≤127

输入样例

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出样例:

5

我们以dp[u][1]表示以u为根节点,且选择u节点的所有方案数,以dp[u][0]表示以u为根节点,且不选择u节点的方案数

对于dp[u][0]如果没有选择了根节点,那么对于他的儿子节点 j 他可以选也可以不选,二者取最大值即可,即dp[u][0]+=max(dp[j][0],dp[j][1])

对于dp[u][1]如果选择根节点,那么对于他的儿子节点 j 他一定不能选,即dp[u][1]+=dp[j][0]

代码如下:

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

using namespace std;

const int N=6010;

int n,happy[N];//happy存储每个人的快乐指数
int h[N],e[N],ne[N],idx;//用邻接表存储每个节点的儿子
int dp[N][2];//dp[u][0]表示以u为根节点且没有选择u节点的方案数,dp[u][1]表示以u为根节点且选择了u节点的方案数
bool has_father[N];//表示一个节点是否有父节点,用来找根节点

//邻接表存储树
void add(int a,int b)
{
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}
//用递归求解
void dfs(int u)
{
    dp[u][1]=happy[u];//如果选择了这个节点,那么要给他附上他的快乐指数
    for(int i=h[u];i!=-1;i=ne[i])//找到这个节点的儿子
    {
        int j=e[i];
        dfs(j);//递归求每个儿子的dp[j][0]和dp[j][1]
        dp[u][0]+=max(dp[j][1],dp[j][0]);//如果没有选择这个根节点,那么对于他的儿子他可以选或不选,取两者最大值
        dp[u][1]+=dp[j][0];//如果选了这个节点,那么对于他的儿子节点他只能不选
    }
}
int main()
{
    memset(h,-1,sizeof h);//邻接表头初始化
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>happy[i];
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;//表示b是a的父节点
        add(b,a);//连接b,a
        has_father[a]=true;//说明a有父节点
    }
    int root=1;
    while(has_father[root])//找到根节点
        root++;
    dfs(root);//以根节点递归求解
    cout<<max(dp[root][0],dp[root][1]);//对于根节点是否选取的两种情况取最大值
    return 0;
}

五,记忆化搜索

AcWing  901.滑雪

题目描述

        trs喜欢滑雪。他来到了一个滑雪场,这个滑雪场是一个矩形,为了简便,我们用r行c列的矩阵来表示每块地形。为了得到更快的速度,滑行的路线必须向下倾斜。         例如样例中的那个矩形,可以从某个点滑向上下左右四个相邻的点之一。例如24-17-16-1,其实25-24-23…3-2-1更长,事实上这是最长的一条。

输入格式

输入文件 第1行:  两个数字r,c(1< =r,c< =100),表示矩阵的行列。 第2..r+1行:每行c个数,表示这个矩阵。

输出格式

输出文件 仅一行:  输出1个整数,表示可以滑行的最大长度。

输入样例 

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出样例 

25

 我们用一个二维数组dp[i][j]表示从点(i,j)开始走的路径长度的集合,集合属性为最大值,可以知道,每个点的路径长度等于从他的四个方向开始走的最大值加一,所以搜索每个点能走到的路径长度最大值,全部值取一个max那么就是整个路径长度的最大值,因为每个点存储的都是以这个点为起点能走到的最大路径长度,所以如果在搜索的过程中遇到了已经搜索过的点,就可以直接返回这个已经搜索过的点的值,不用再重新搜索一遍了,这就是记忆化搜索

代码如下:

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

using namespace std;

const int N = 110;

int n,m;
int h[N][N];//表示高度
int dp[N][N];//表示(i,j)这个点出发的路径长度的最大值

int dx[4] = { 0,1,0,-1 };//定义偏移量,右下左上四个方向
int dy[4] = { 1,0,-1,0 };

int dfs(int x, int y)
{
	int& v = dp[x][y];//引用起别名的方式,这样后面代码会短一点
	if (v != -1)//如果这个点被找过了,就直接返回不用再往后找了,因为每个点都存的是以这个点能走到的路径长度最大值
		return v;
	v = 1;//如果这个点没找过将这个点的路径长度设置为1
	for (int i = 0; i < 4; i++)//右下左上四个方向寻找
	{
		int tx = x + dx[i];
		int ty = y + dy[i];
		if(tx>=0&&tx<n&&ty>=0&ty<m&&h[tx][ty]<h[x][y])//满足条件的往下搜索
			v=max(v,dfs(tx, ty)+1);//v的路径长度等于他往下滑的那个点的长度加1
	}
	return v;
}
int main()
{
	memset(dp, -1, sizeof dp);//初始化为-1,表示初始时每个点都还没被找过
	cin >> n>>m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			cin >> h[i][j];
	int res = 0;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			res = max(res, dfs(i, j));//从每个点开始搜索,取全部点的路径长度最大值
	cout << res;
	return 0;
}

六,数位统计DP

AcWing 338. 计数问题

题目描述

给定两个整数 a 和 b,求 a 和 b之间的所有数字中 0~9 的出现次数。

例如,a=1024,b=1032,则 a 和 b之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…

输入格式

输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a和 b。

当读入一行为 0 0 时,表示输入终止,且该行不作处理。

输出格式

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

每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。

数据范围

0<a,b<100000000

输入样例:

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例:

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247

 这题我们用前缀和的思想 ,用一个count函数求出来1到n中x的出现次数,我们求1到n中x得出现次数是通过求x在每一位出现得次数的总和

那么a到b的x的出现次数就等于count(b,x)- count(a-1,x)

接下来我们要分类讨论,看下面这个图

解释一下上面这个图,假设有一个七位数n,位abcdefg,我们想求1在第四位上出现的次数那么就要分情况讨论

1,当我们前三位xxx取得数小于abc时,那前三位可以取得数就是000~abc-1,后三位yyy可以取得数就是000~999,所以所有得情况就是abc*1000

2,当我们前三位xxx取得数刚好等于abc时,这时又要分情况讨论

        1)如果 n 得第四位d大于1得话,那我们得后三位数yyy就可以在000~999中随便取,就是1000种情况

        2)如果 n 得第四位刚好等于1得话,那么我们后三位数只能取000~efg,否则取得数会大于n,那么就是efg+1种情况

        3)如果 n 得第四位小于1得话,那么我们后三位无论取什么,1在第四位出现得次数始终是0,那么只有0种情况

最后还要考虑一下当x等于0时,也就是求0在各位出现得情况,当某一位取0时,他的前面必须要大于0,比如说求n 中0在第四位出现得次数时,xxx得取值就不是000~999,而是001~999了,因为不存在0000efg这样得数,这样得话不能表示0出现在第四位得情况

还有就是求某一个数x出现在最高位时得情况,那就不用考虑第一种情况了

至此,所有得情况就考虑完了,代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;

const int N = 10;

int power(int x)//求十的x次方
{
	int res = 1;
	while (x--)
		res *= 10;
	return res;
}
int get(vector<int> v, int l, int r)//求出第i位前的数
{
	int res = 0;
	for (int i = l; i >= r; i--)
		res = res * 10 + v[i];
	return res;
}
int count(int n, int x)//求1~n中x的出现次数
{
	if (!n)//如果n为0,直接返回0,其实这一步加不加都可以,因为题目给的范围是大于0的
		return 0;
	vector<int>v;//存储n的每一位数字
	while (n)//将n分解成一位一位数存储起来
	{
		v.push_back(n % 10);
		n /= 10;
	}
	n = v.size();//将n改成n的长度
	
	int res = 0;//存储答案,即x的出现次数
	
	for (int i = n - 1-!x; i >= 0; i--)//因为存下来是逆序的,所以这里要倒着遍历,求x在每一位出现得次数
	{   //解释一下i=n-1-!x,表示当x位0时,从i=n-2开始遍历,因为0不可能出现在最高位上

		if (i < n - 1)//表示x不是出现在最高位上
		{
		    //那么res就等于第i位前的数乘上十的(第i位后面的数的个数)次方
			res += get(v, n - 1, i + 1) * power(i);
			
			if (!x)//如果x是0的话,第i位前的数必须从1开始,就要少一个10的i次方
				res -= power(i);
		}
		if (v[i] == x)//如果n的第i位恰好等于x,那么所有的情况只能是第i位后面的数的大小+1的情况
			res += get(v, i - 1, 0) + 1;
		else if (v[i] > x)//如果n的第i位大于x,那i后面有几个数,就是有多少十的几次方种情况
			res += power(i);
	}
	return res;
}
int main()
{
	int a, b;
	while (cin >> a >> b, a || b)
	{
		if (a > b)
			swap(a, b);//如果a大于b要交换,省去后面的判断
		for (int i = 0; i <= 9; i++)
			cout << count(b, i) - count(a - 1, i) << " ";
		//利用前缀和的思想b中i的出现次数减去a-1中i的出现次数,就是a到b中i的出现次数的总和
		cout << endl;
	}
	return 0;
}

虽然代码看着有些复杂,但实际上运行得效率是很高得,时间复杂度为10*2*8*10=1600,非常得快

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值