[做题]动态规划

文章目录

  • 0.DP前言
  • 1.线性DP
    • 数字三角形
      • 朴素做法
      • 更优雅的写法
      • 一维优化(朴素版本)
    • 最长上升子序列
      • 朴素做法
      • 二分(数据加强版)
    • 最长公共子序列
      • 朴素做法
    • 最短编辑距离
      • 朴素做法
    • 编辑距离
      • 朴素做法
  • 2.其他DP
    • 区间dp
      • 石子合并
    • 计数dp
      • 整数划分
        • dp写法:
        • 完全背包解法:
    • 数位统计dp
      • 计数问题
    • 树形dp
      • 没有上司的舞会
    • 记忆化搜索
      • 滑雪
    • 状态压缩dp
      • 蒙德里安的梦想
      • 最短Hamilton路径

0.DP前言

下面一系列的线性dp问题更依赖闫氏dp分析法的发挥。

这里记录几条经验:

  1. 当状态计算方程中,在遍历时出现了 f[i-1] 的字样,那么数组下标就要从1开始,来防止负数下标和越界。

  2. 状态表示,我们划分集合的原则为 “不重不漏”:不重复,不遗漏

    当状态属性 要求取 sum 集合的总和时,不重复的原则很重要。

    但状态属性取 max/min 集合的最值时,集合间元素发生重复不会影响结果。

    举个例子: 1,2,3

    • 求最值:max = max(max (1,2) , max (2,3) )。 这里2发生了重复,但不影响最大值3的求取。

    • 求总和:sum = sum(sum (1,2) , sum(2,3) )。 这里2发生了重复,影响了总和的求取(2被加了两次)

  3. 状态表示分为三部分:

    • 集合的维度:一维数组 f[i] 能求出答案吗?不能的话就用二维数组f[i] [j] ,还不行就三维。
    • 集合的表示:每个集合,它的意义是什么?这个意义要能概括题目的所有情况。
    • 集合的属性:求sum还是max?属性有时会影响状态计算方程的展开。
  4. 一个简单的数学模型:

    集合A:a1,a2,a3 … an

    集合B:a1+w,a2+2, … an+w

    则有:min(A)+w = min(B)

    或 :min(A) = min(B) - w

  5. C++语言,一秒处理数据大小为 108左右,尽量把复杂度控制在107,最多10^8

1.线性DP

数字三角形

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

DP分析

状态表示:

1

状态转移:

朴素做法

#include<iostream>
using namespace std ;
const int N = 510 , INF = 1e9;int f[N][N];
int a[N][N];
int n;int main(void)
{//初始化数据cin>>n;for(int i=1;i<=n;i++){for(int j=1;j<=i;j++){scanf("%d",&a[i][j]);}}//dp数组赋初值 (下标从1开始,不会产生越界问题。但下标为0的数据要提前赋初值,也就是INF)for(int i=0;i<=n;i++){for(int j=0;j<=i+1;j++){ //初值范围是[0,i+1] ,左右端点 0和i+1 都会用到。f[i][j] = -INF;}}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]+a[i][j],f[i-1][j-1]+a[i][j]);}}//输出int res = -INF;for(int i=1;i<=n;i++) res = max(res,f[n][i]);cout<<res;}

更优雅的写法

#include <bits/stdc++.h>using namespace std;const int N = 510;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 >> f[i][j];for (int i = n - 1; i >= 1; i--)for (int j = 1; j <= i; j++)f[i][j] = max(f[i + 1][j + 1], f[i + 1][j]) + f[i][j];cout << f[1][1] << endl;
}

该做法是从最下方,向上推。

朴素做法是从第二层,向下推。

一维优化(朴素版本)

 #include <iostream>
#include <algorithm>using namespace std;const int N = 510, INF .....................................................................= -1e9;int a[N][N];
int f[N];
int n, m;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 + 1; i ++ ) f[i] = INF;f[1] = a[1][1];for (int i = 2; i <= n; i ++)for (int j = i; j >= 1; j --)f[j] = max(f[j] + a[i][j], f[j - 1] + a[i][j]);int res = INF;for (int i = 1; i <= n; i ++) res = max(res, f[i]);cout << res << endl;return 0;}

最长上升子序列

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

朴素做法

闫氏dp:

状态表示: f[i] , 1.集合:所有以第i个数字结尾的上升子序列

​ 2.属性:max

状态计算:(注意,本题dp未用到 i-1 的递增公式,故 i 的下标从0开始)

​ 分析集合: ( 0 | 1 | 2 | 3 |…| i-1)

在该集合中,题目要求数字大小逐渐上升,即 An-1< An 。

如果出现An-1>=An的情况,我们不会把它包含在集合中去,换言之,所求的都是满足题意的某个集合的最大值。

对于形如 AiAj 的上升子序列,可知f[j]的最长上升子序列,即f[i]的最长上升子序列+1。

故计算方程:f[ i ] = max( f[ j ] +1 ), j = 0,1,2,3… i-1 且 ( a[i]>a[j] ) 。

二分(数据加强版)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

思路:

在朴素做法的基础上,我们需要追加一个集合,再次分析,来达到优化剪枝的效果。

分析:以 3 1 4 8 5 为例。 如果一个子序列的长度为1,那该序列的末尾值可以取 1 ,也可以取 8。

如果要最长子序列,那末尾值就必须最小,也就是min(1,8) = 1 ,取1不取8。

为了实现该功能:

追加一个数组q[i] ,这个数组表示长度为i时,结尾最小的序列值为q[i]。所以该数组有效值的长度,就是最长上升子序列的长度。

遍历a数组里面的数,然后在q(这个数组的长度就是答案)这个数组里面查找是否存在一个大于且最靠近他的数,
若果不存在话,说明这个数是在q所有的数中是最大的
(这时下标也已经扫描到q数组的最右边,r这个下标已经定位到q的最右边,将其+1,更新q长度(即:更新答案)和新元素的值),
若存在(即:该点的值不是上升地),r+1也不会增加q的长度(因为不是上升的情况,所以这里答案不会更新),说明该点最长子序列一定是前面其中的某个答案,只需要将里面地最靠近的那个元素进行更新即可

#include <iostream>
#include <algorithm>using namespace std;const int N = 100010;int n;
int a[N] = {7,3,4,5,1,8,3};
int q[N];int main()
{n=7;int len = 0;for (int i = 0; i < n; i ++ ){int l = 0, r = len;while (l < r){//  q里有无大于当前a[i]:  存在原地,不存在的话更新 //                        不存在的话就是 r = 0//                         存在的话就是 r != 0 ,即大于1的线段,更新len //                 (当然,如果有多个大于a[i]的数,则选择最靠近a[i]的。) int mid = l + r + 1 >> 1;if (q[mid] < a[i]) l = mid;// check()  else r = mid - 1;   }len = max(len, r + 1);q[r + 1] = a[i];   printf("i = %d ,r = %d ,a[i]=%d , len = %d;  ",i,r,a[i],len);for(int i=0;i<n;i++) cout<<q[i]<<" ";cout<<endl;}    ///a[N] = {7,3,4,5,1,8,3};for(int i =0;i<n;i++) printf("q%d=%d\n",i,q[i]);cout<<endl; printf("%d\n", len);return 0;
}

注:此代码并非最终题解,因为保留了调试部分的输出语句和注释。

结果展示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最长公共子序列

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

闫氏dp:

状态表示: f[i] [j] , 1.集合:字符串A的前i个字符,和字符串B的前j个字符的公共子序列

​ 2.属性:max

状态计算:

以两个字符串的末位字符,即A[i] 和 B[j] 划分,有四种情况:

  • 00 :最长公共子序列的末尾字符,即不是A[i],也不是B[j] 。所以很自然,有 f(i,j) = f(i-1,j-1)

  • 11 : 最长公共子序列的末尾字符,即是A[i],又是B[j]。所以必然有A[i] == B[j] 且f(i,j) = f(i-1,j-1)+1

  • 01 : 末位不是A[i] ,但却是B[j]。经下面的分析,此种情况为 f(i,j) = f(i-1,j)

    这里我们会想当然认为,f(i,j) = f(i-1,j)。但不是这样的。因为f(i-1,j) 同样包含了 不选择B[j]的情况,这与我们的集合情况不符。但由于此题求的是最大值,根据前言原则2,所以重复不影响结果。

  • 10 : 与01同理,重复无所谓,全覆盖就行。此种情 况为 f(i,j) = f(i,j-1)

​ 分析集合: ( 00|01|10|11)

​ 集合00是被包含在01和10的集合中的。所以在状态转移方程中省去00。

状态转移方程:

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);

朴素做法

枚举i,j —— 状态转移

#include<iostream>
using namespace std ;
const int N = 1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main(void)
{cin>>n>>m>>(a+1)>>(b+1); //下标从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);}}printf("%d",f[n][m]);}

最短编辑距离

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

朴素做法

闫氏dp:

状态表示:f(i,j)

​ 1.集合:将A[1~i] 操作后与B[1~j]相等的操作方式 的集合

​ 2.属性:min

状态计算:

( 删 | 增 | 改 )

  • 删:删除之后匹配。删除多余的A[i],即前面的最小值+1即现在的最小值。则有f(i,j ) = f(i-1,j)+1 ;

  • 增:增加之后匹配。即A增加的值恰好是B[j] ,则有f(i,j) = f(i,j-1)+1 ;

  • 改:更改(末尾值A[i])之后匹配。即要保证A[1~i-1] 和 B[1~j-1]是匹配的,再去改末位。

    则有f(i,j) = f(i-1,j-1)+1

    注意:这一步要判断。当A[1i-1]与B[1j-1]匹配时,如果A[i]==B[j] 那就不需要再改末位,所以此时情况是f(i,j) = f(i-1,j-1)

代码:

#include<iostream>
using namespace std ;
const int N = 1010;
int f[N][N] ; //dp数组要建好
char A[N],B[N];int main(void)
{int n,m;scanf("%d%s",&n,A+1);scanf("%d%s",&m,B+1);//dp前的初始化for(int i=0;i<=m;i++) f[0][i] = i; //当A的字符串是空,B的字符串长度为i,操作数即i(第i步增加B[i]),i<=m B的长度for(int j=0;j<=n;j++) f[j][0] = j; //当B的字符串是空,A的字符串长度为i,操作数即j (第j步删除A[i]) ,j<=n A的长度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);}}//输出结果:把A的前n个字母变成B的前m个字母cout<<f[n][m];
}

编辑距离

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

时间复杂度 :n个字符串 , 每个字符串有 m次询问 n<=1000,m<=1000
字符串长度为10,比对时是n^2 ,即10^2 = 100

所以复杂度为: 1000 * 1000 * 100 = 10^8 , 有点紧。

给定 n个字符串,和当前字符串进行比较,求边界距离。

关键点在于求边界距离

边界距离的求法,可参考上一题“最短编辑距离“

朴素做法

#include<iostream>
#include<cstring>
using namespace std ;
const int N = 1010;
const int M = 15; //10
int f[N][N]; //dp数组
char str[N][M];//字符串数组,存储询问//这里是最短编辑距离的算法。
int edit_distance(char a[],char b[]){int la = strlen(a + 1), lb = strlen(b + 1);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);f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));}return f[la][lb];
}int main(void)
{int n,m;cin>>n>>m;for(int i=0;i<n;i++){scanf("%s",str[i]+1);}while(m--){  //开始询问:求给定 n 个字符串中,谁可以在上限操作次数内变成询问给出的字符串。char s[M];int limit; //上限次数scanf("%s%d",(s+1),&limit);int res = 0;for(int i=0;i<n;i++)  //遍历“给定字符串”if(edit_distance(str[i],s) <= limit ) //在边界范围内res++;cout<<res<<endl;}return 0;
}

2.其他DP

区间dp

石子合并

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

数据范围 : 1≤N≤300

闫氏dp分析:

状态表示:f(i,j)

  1. 集合 :所有将[i,j]合并成一堆的集合

    j-i+1的块,共 !(j-i) 种可能的情况

  2. 属性 :min

状态计算: ( i |i+1|i+2| … | j-1 | j | )

f(i,j) 划分依据为左边的最后一堆 的下标

每一类的最小值 中,再取最小值即答案。

min = ( min (i,k) , min (k+1,j) )

分析第 k 类: f (i,j) = f(i,k) + f(k+1, j) + s[ j ] - s[i-1]

求权值时,用前缀和优化一下

#include<iostream>
using namespace std; 
const int N = 305;
int f[N][N];
int s[N];
int n;
int main(void)
{cin>>n;for(int i=1;i<=n;i++){scanf("%d",&s[i]);}for(int i=1;i<=n;i++){s[i] += s[i-1];}//区间dp 第一步:枚举区间长度  (区间长度为1不用枚举)for(int len = 2;len<=n;len++) for(int l=1;l+len-1<=n;l++){ //最后只剩一堆,所以是<=nint r = l+len-1;f[l][r] = 1e8;              //截k点for(int k=l;k+1<=r;k++){  //对于[l, k] k可以取到 l 对于[k+1, r] ,因为k+1 <= r, 所以 k <= r - 1, 即 k < rf[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);}}cout<<f[1][n];
}
f[l] [r]  = 1e8

这一步相当于初始化所有k的集合为最大值,这些集合还没有赋值。

答案则是在所有赋值后的k的集合中取最小值

复杂度: n * n *k , O(n^3) 。

n<=300 , 两百七十万的运算量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

计数dp

整数划分

一个正整数 n可以表示成若干个正整数之和,形如:n=n1+n2+…+nk

其中 n1≥n2≥…≥nk, k≥1。

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能很大,输出结果请对 109+7109+7 取模。

数据范围

1≤n≤1000

输入样例:

5

输出样例:

7
dp写法:
  • 状态表示:

    • 集合 :f( i , j ) : 所有总和是 i ,恰好表示成 j 个数的和的方案

    • 属性 :sum

  • 状态计算:

​ f(i , j) : 根据最小值是1还是最小值大于1划分

​ >(最小值是1|最小值大于1)


最小值是1:数量等同于f[i-1,j-1]

也就是总和是 i-1,用 j-1 个数表示的方案

最小值大于1 :数量等同于 f( i-j , j)

大于1的每个数,减去一个1,还是有j个数。

所以综上,sum = f(i , 1) + f(i , 2) + f(i , 3) +... f(i , i) .

const int N = 1010, mod = 1e9 + 7;int n;
int f[N][N];int main()
{cin >> n;f[1][1] = 1;for (int i = 2; i <= n; i ++ )for (int j = 1; j <= i; j ++ )f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;int res = 0;for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;cout << res << endl;return 0;
}
完全背包解法:

把这题抽象成,一个容量为 n 的背包,有 n 个物品 :体积分别是1、2、 3 … n 。

  • 状态表示:

    • 集合 :f( i , j ) : 所有从1到 i 中选,体积恰好是 j 的选法的数量
    • 属性 :sum
  • 状态计算:

    • f(i , j) : 根据最后一个物品选择了几个来定义划分区间。

(0|1|2|3|...|s)

f(i-1,j) | f(i-1,j-i)| f(i-1, j-2i)|...| f(i-1,j - s i)

故有:

f[i] [j]   =    f[i-1] [j] + f[i-1] [j-i] + f[i-1] [j-2i]+... f[i-1] [j-si]   //1
f[i] [j-i] =			     f[i-1] [j-i] + f[i-1] [j-2i]+... f[i-1] [j-si] //2

1-2 = f[i] [j] - f[i] [j-i] = f[ i-1 ] [j]

所以可得 f[i] [j] = f[i] [j-i]+ f[ i-1 ] [j]

同完全背包一样,压缩成一维:

f[j] = f [j-i]+ f [j]

#include<iostream>
using namespace std;const int N = 1010;
int n;
int f[N][N]; // 使用前i个数恰好能组成数j的方案数
int mod = 1e9 + 7;int main(){cin>>n;for(int i = 0; i <= n; i++)  f[i][0] = 1; // 使用前i个数组成0,每一个都有1种解法(都不选)//f[0][0] = 1;for(int i = 1; i <= n; i++)for(int j = 1; j <= n; j++){// 注意,使用二维时需要一个优化,需要考虑当i比j小时的情况和i>=j的情况://如果 i>j,就意味着一个也选不上,只能和 (i,i-1) 相同。f[i][j] = (f[i-1][j]) % mod; if(j >= i) f[i][j] = (f[i][j] + f[i][j-i]) % mod; // or  f[i][j] = (f[i][j-i] + f[i-1][j])%mod;}cout<<f[n][n];return 0;
}
/*
一维:cin>>n;for(int i=1;i<=n;i++) 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];
*/

数位统计dp

计数问题

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

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

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 1010 次,1 出现 1010 次,2 出现 77 次,3 出现 33 次等等…

输入格式

输入包含多组测试数据。

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

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

输出格式

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

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

数据范围

0<a,b<100000000


核心思想:分情况讨论

核心函数 :count(n,x) : 计算 1~n ,x出现的次数。

题解即 count(b,x) - count(a,x) 。

1~n , x =1

n = abcdefg

分别求出1在每一位上出现的次数

举例:

求1在第4位上出现的次数:

1<=xxx1yyy <= abcdefg

  • xxx = 000~abc-1 , yyy = 000~999 :abc*1000
  • xxx=abc :
    1. d<1 , abc1yyy > abc0efg : 0
    2. d=1, yyy=000~efg : efg+1
    3. d>1, yyy=000~999 : 1000

复杂度:10 * 2 * 8 * 10 = 1600

10个数字,2次调用函数,8位 , 10次函数内循环

边界问题:

枚举数字在最高位,情况一 不存在

2.枚举数字 0 ,从001开始取到 abc 。因为不能有前导0。

#include <iostream>
#include <algorithm>
#include <vector>using namespace std;const int N = 10;int get(vector<int> num, int l, int r)  //数字 l~r 位的数字
{int res = 0; //注意:由于vector是逆序存数,所以这里也要逆序for (int i = l; i >= r; i -- ) res = res * 10 + num[i];return res;
}int power10(int x)   // return 10^x
{int res = 1;while (x -- ) res *= 10;return res;
}int count(int n, int x) //1~n 中 x出现的次数
{if (!n) return 0; //1~0不计数vector<int> num;while (n) //反转数组,从右往左数{num.push_back(n % 10);n /= 10;}n = num.size();//记位数int res = 0;    //special 1 ↓for (int i = n - 1 - !x; i >= 0; i -- )  //从最左边开始遍历计数:求x在第i位的数量{  													//(针对所有范围内的抽象数)if (i < n - 1){res += get(num, n - 1, i + 1) * power10(i);if (!x) res -= power10(i); //special 2}//这里上下是两种情况,上为x左位是 00...x~ab..x的情况,下为x..~x..ef的情况//逻辑上是两个并列循环,循环条件相同所以放在一个循环中。//1.num[i] < x:0 不计数,不考虑if (num[i] == x) res += get(num, i - 1, 0) + 1;//2.else if (num[i] > x) res += power10(i);}return res;
}int main()
{int a, b;while (cin >> a >> b , a){if (a > b) swap(a, b);for (int i = 0; i <= 9; i ++ )cout << count(b, i) - count(a - 1, i) << ' ';cout << endl;}return 0;
}

count函数里,两个!x出现的位置即对于两个特殊情况的解。

1.枚举数字在最高位,情况一 不存在。

for (int i = n - 1 - !x; i >= 0; i -- ) 

最左位不能是 0 (前置0),如果 x 为 0,则遍历从 n-2开始;不为 0 则从 n-1开始。

2.x==0时 ,需要从001开始取到 abc

if (!x) res -= power10(i); 

注意:

count(b, i) - count(a - 1, i) << ' ';res += get(num, n - 1, i + 1) * power10(i);if (num[i] == x) res += get(num, i - 1, 0) + 1;

这三个边界是否要 +1、-1 很容易出差错。

树形dp

没有上司的舞会

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

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

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

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

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

输入格式

第一行一个整数 N。

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

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

输出格式

输出最大的快乐指数。

数据范围

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


  • 状态表示:f(u, 1/0)

    • 集合 :

      f( u , 0 ) : 从以u为根的子树当中选择,并且不选 u 的方案 ;

      f( u , 1 ) : 从以u为根的子树当中选择,并且选 u 的方案。

    • 属性 :max

  • 状态计算:

    • f( u , 0 ) : 不选 u,为了最大值,要选择子树中的最大值。(对每个子树,既可以选,也可以不选)

      f(u , 0) = ∑ max( f( si,1 ) , f( si , 0) )

    • f( u , 1 ) : 已经选u了,子树不能选了。

      f(u,1) = ∑ f(si,0)

时间复杂度:每个结点状态计算的是它的儿子,所以计算所有状态相当于遍历一遍,即 O(n)

此题画图更好理解。

类似状态机,但和状态机没什么关系。

#include <cstring>
#include <iostream>
#include <algorithm>using namespace std;const int N = 6010;int n;
int happy[N]; //开心值
int h[N],e[N],ne[N],idx;//邻接表
bool has_fa[N];//判断有无父节点,从而寻找根节点
int f[N][2]; //dp喽void add(int a, int b)
{e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}void dfs(int u)
{f[u][1] = happy[u];for (int i = h[u]; ~i; i = ne[i]) //~i <=> i!=-1{int j = e[i]; dfs(j);f[u][1] += f[j][0];f[u][0] += max(f[j][0], f[j][1]);}
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);memset(h, -1, sizeof h);for (int i = 0; i < n - 1; i ++ ){int a, b;scanf("%d%d", &a, &b);add(b, a);has_fa[a] = true;}int root = 1;while (has_fa[root]) root ++ ;dfs(root);printf("%d\n", max(f[root][0], f[root][1]));return 0;
}

记忆化搜索

滑雪

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

 1  2  3  4 516 17 18 19 615 24 25 20 714 23 22 21 813 12 11 10 9

在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 2525 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

输入格式

第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

输出格式

输出一个整数,表示可完成的最长滑雪长度。

数据范围

1≤R,C≤300
0≤矩阵中整数≤10000

输入样例:

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

  • 状态表示:f ( i , j ) 二维路径嘛。

    • 集合 :所有从( i,j )开始滑的路径中的最大值
    • 属性 :max
  • 状态计算:

    ( ↑ | ↓ | ← | → )

时间复杂度:每个结点状态计算的是它的儿子,所以计算所有状态相当于遍历一遍,即 O(n)

存在一个最大值,如果所有结点的值都减去固定值,该值仍然作为最大值。

同理:如果你要求 (i , j) 点的最大值,假设向右遍历,则需要 (i+1, j ) 的最大值,也就是所谓的 :

f(i , j) = f( i+1 , j) +1

但向右遍历只是一种情况。

考虑四种情况的式子为:f[i][j] = max(f[i][j],f[tx][ty]+1);

举个例子:

对于 25−24−23−…−3−2−1 ,这个路径的求取过程就是从右往左。

递归程序从 25 调用,一直到 1,再从右往左 加和 并回溯。

#include<iostream>
#include<memory.h>
using namespace std ;
const int N = 310;int n,m;
int g[N][N];
int f[N][N];
int dir[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
int dp(int x,int y)
{int &v = f[x][y];if(v!=-1) return v; //每个点只求一次路径最大值v = 1;for(int i=0;i<4;i++){int tx = x + dir[i][0], ty = y + dir[i][1];if(tx>=1&&ty>=1&&tx<=n&&ty<=m&&g[tx][ty]<g[x][y])v = max(v,dp(tx,ty)+1);}return v;
}
int main(void)
{cin>>n>>m;for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>g[i][j];memset(f,-1,sizeof f);int res =0;for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){res = max(res,dp(i,j)); //dp:所有以(i,j)为起点滑的路径中的最大值}cout<<res;return 0;
}

状态压缩dp

蒙德里安的梦想

求把 N×M 的棋盘分割成若干个 1×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


最短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,zx,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]≤107

输入样例:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/757451.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI - 机器学习GBDT算法

目录 GBDT 提升树 梯度提升树 GBDT算法实战案例 XGBoost &#x1f606;&#x1f606;&#x1f606;感谢大家的观看&#x1f606;&#x1f606; GBDT 梯度提升决策树&#xff08;Gradient Boosting Decision Tree&#xff09;&#xff0c;是一种集成学习的算法&…

手机投屏到电脑

手机投屏到电脑 Github 有2个开源的手机投屏项目&#xff1a; Scrcpy: https://github.com/Genymobile/scrcpy QtScrcpy: https://github.com/barry-ran/QtScrcpy 这2个项目都很好用&#xff0c;我这里用的是 Scrcpy&#xff1a; 官方文档中介绍了如何在windows上使用 Scrcpy…

基于龙芯2k1000 mips架构ddr调试心得(一)

1、基础知识 DDR2的I/O频率是DDR的2倍&#xff0c;也就是266、333、400MHz。 DDR3传输速率介于 800&#xff5e;1600 MT/s之间 DDR4的传输速率目前可达2133&#xff5e;3200 MT/s 2k1000内存&#xff1a;板载2GB DDR3 &#xff0c;可选4GB 使用龙芯芯片最好用他们自己的Bo…

C++特性三:多态的基本语法及原理剖析

一、多态的基本语法 多态分为两类 静态多态: 函数重载 和 运算符重载属于静态多态&#xff0c;复用函数名 动态多态: 派生类和虚函数实现运行时多态 静态多态和动态多态区别&#xff1a; 静态多态的函数地址早绑定 - 编译阶段确定函数地址 动态多态的函数地址晚绑定 - 运…

【Python】使用selenium对Poe批量模拟注册脚本

配置好接码api即可实现自动化注册登录试用一体。 运行后会注册账号并绑定邮箱与手机号进行登录试用。 测试结果30秒一个号 import re import time import requests from bs4 import BeautifulSoup from selenium import webdriver from selenium.webdriver.chrome.options imp…

keithley2612A数字源表

181/2461/8938产品概述&#xff1a; Keithley 2612A源表既可用作台式I-V表征工具&#xff0c;也可用作多通道I-V测试系统的构建模块组件。对于台式使用&#xff0c;吉时利2612ASourceMeter具有嵌入式TSP Express软件工具&#xff0c;允许用户快速轻松地执行常见的I-V测试&…

微信小程序开发学习笔记——4.2showModal和showLoading界面交互操作

>>跟着b站up主“咸虾米_”学习微信小程序开发中&#xff0c;把学习记录存到这方便后续查找。 课程连接&#xff1a;https://www.bilibili.com/video/BV19G4y1K74d?p27&vd_source9b149469177ab5fdc47515e14cf3cf74 一、showModal 显示模态对话框 1、属性 https:/…

基于springboot+vue的摄影网站(源码+部署说明+系统介绍+数据库)

作者主页&#xff1a;Java程序员老张 主要内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;…

SSTI漏洞详解

目录 前备知识 模块引擎&#xff1a; 模块渲染函数&#xff1a; 继承关系&#xff1a; SSTI漏洞简介 SSTI漏洞成因 SSTI漏洞原理 一些常见模块介绍 php Twig模块引擎 代码演示1 Twig模块引擎代码演示2 python flask模块 代码演示1&#xff1a; python jinja模块 代…

深入理解Java并发工具包中的CyclicBarrier

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! 在Java的并发编程世界中&#xff0c;协调和管理多个线程的执行是一项复杂而关键的任务。为了简化这一挑战&#xff0c;Java并发包…

数据分析-Pandas序列滑动窗口配置参数

数据分析-Pandas序列滑动窗口配置参数 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&…

初探Springboot 参数校验

文章目录 前言Bean Validation注解 实践出真知异常处理 总结 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 前言 工作中我们经常会遇到验证字段是否必填&#xff0c;或者字段的值是否…

极客早报第3期:罗斯否认插足凯特王妃婚姻;清明放假调休3天;国产伟哥去年销售近13亿

一分钟速览新闻点&#xff01; 每日简报 罗斯否认插足凯特王妃婚姻清明放假调休3天国产伟哥去年销售近13亿男子持台球杆殴打2名女店员被抓今日春分淀粉肠小王子带货日销售额涨超10倍[高中生被打伤下体休学 邯郸通报](https://www.baidu.com/s?wd高中生被打伤下体休学 邯郸通报…

ARM Cortex-R82处理器在压缩SSD场景的应用

ScaleFlux公司宣布在其下一代企业级SSD控制器产品线中采用Arm公司的Cortex-R82处理器。这一决策旨在应对企业环境中对高带宽存储解决方案日益增长的需求&#xff0c;并通过提升数据传输速度和效率来满足市场期待。 Arm Cortex-R82处理器是Arm公司迄今为止性能最强的实时处理器…

STC 51单片机烧录程序遇到一直检测单片机的问题

准备工作 一&#xff0c;需要一个USB-TTL的下载器 &#xff0c;并安装好对应的驱动程序 二、对应的下载软件&#xff0c;stc软件需要官方的软件&#xff08;最好是最新的&#xff0c;个人遇到旧的下载软件出现问题&#xff09; 几种出现一直检测的原因 下载软件图标&#xf…

Unbuntu20.04 git push和pull相关问题

文章目录 Unbuntu20.04 git push和pull使用&#xff11;&#xff0e;下载[Git工具包](https://git-scm.com/downloads)&#xff12;&#xff0e;建立本地仓库&#xff13;&#xff0e;将本地仓库与github远程仓库关联&#xff14;&#xff0e;将本地仓库文件上传到github远程仓…

J4G企业通讯ip电话 sip对讲主机 停车场对讲主机

J4G企业通讯ip电话 sip对讲主机 停车场对讲主机 SV-J4G 是一款企业级彩屏网络电话&#xff0c;具有高清语音&#xff0c;320x240 2.8英寸彩屏&#xff0c;支持千兆以太网&#xff0c;12个SIP账号&#xff0c;支持PoE供电&#xff0c;支持外接EHS无线耳机&#xff0c;三方电话会…

Grok-1:参数量最大的开源大语言模型

Grok-1&#xff1a;参数量最大的开源大语言模型 项目简介 由马斯克领衔的大型模型企业 xAI 正式公布了一项重要动作&#xff1a;开源了一个拥有 3140 亿参数的混合专家模型&#xff08;MoE&#xff09;「Grok-1」&#xff0c;连同其模型权重和网络架构一并公开。 此举将 Gro…

清华大模型ChatGLM3部署初体验

正文共&#xff1a;1555 字 17 图&#xff0c;预估阅读时间&#xff1a;2 分钟 ChatGLM3是智谱AI和清华大学KEG实验室联合发布的对话预训练模型。该项目在GitHub的工程链接为&#xff1a; https://github.com/THUDM/ChatGLM3 在人工智能领域中&#xff0c;类似“3B”、“6B”、…

C++--STL标准库

一.模板 模板是C中泛型编程的基础。一个模板就是一个创建类或函数的蓝图。 生活中常见的模板有: 编写一个比较两个值大小的函数&#xff0c;如果第一个值大于第二个值返回大于0的数字,两个值相等返回0,第一个值小于第二个值返回小于0的数字。 我们可以根据值类型定义多个函数&…