动态规划入门之dp递推~

欢迎访问https://blog.csdn.net/lxt_Lucia~~

宇宙第一小仙女\(^o^)/~~萌量爆表求带飞=≡Σ((( つ^o^)つ~ dalao们点个关注呗~~

本篇文章重在递推,不在总结多种dp(太多了.....慢慢学叭......毕竟我还很菜emmm....)。

在总结递推之前,先提一点基础理论问题叭~

一.描述

1.动态规划的定义:


       动态规划是运筹学的一个分支,是求解决策过程的最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。在各种算法中,我认为动态规划是较难掌握好的,主要难在模型的建立。

2.动态规划算法依赖的两个性质:

(1)最优子结构:问题的最优解是由最优子问题的最优解推出的,也就是问题的最优解包含了子问题的最优解。

(2)重叠子问题:在用递归算法自顶向下解问题时,产生的子问题并非总是新问题,有些被反复计算了多次。

3.算法分析中动态规划的四个基本步骤:

(1)描述优解的结构特征。 

(2)递归地定义一个最优解的值。 

(3)自底向上计算一个最优解的值。

(4)从已计算的信息中构造一个最优解。

4.常用的解题步骤:

(1)  确定子问题: 在这一步重点是分析那些变量是随着问题规模的变小而变小的, 那些变量与问题的规模无关。 
(2)  确定状态:根据上面找到的子问题来给你分割的子问题限定状态 。
(3)  推到出状态转移方程:这里要注意你的状态转移方程是不是满足所有的条件, 注意不要遗漏。 
(4)  确定边界条件:先根据题目的限制条件来确定题目中给出的边界条件是否能直接推导出, 如果不行也可以尝试从边界条件反推(举个例子:a(n)→a(2)有递推关系, 但是a(2)→a(1)不符合上述递推关系, 我们就可以考虑用a(1)来倒推出a(2),然后将递推的终点设置为a(2))。
(5)  确定实现方式:依照个人习惯,就像是01背包的两层for循环的顺序 。
(6)  确定优化方法:很多时候你会发现走到这里步的时候你需要返回第1步重来。首先考虑降维问题(优化内存),优先队列、四边形不等式(优化时间)等等。

5.常用方法:

(1)模型匹配法:与模型相似,直接根据题目变化套用模板,如LIS、LCS、01背包、完全背包、区间模型、树状模型。

(2)三要素法: 
         a.阶段:要先确定阶段,如数塔问题可以先确定当前选的是第几层,每层就是一个阶段。 
         b.状态:确定阶段后,每个阶段中又分为许多不同的状态。 
         c.决策:如背包中,用1和0来标记选还是不选第i种物品。
(3)寻找规律法:从小的状态开始推,慢慢总结出规律,或者先暴力打表,以减少繁琐,有时还真的就是暴力出奇迹哈哈。

(4)增加约束条件法:对应着消除后效性。

(5)边界条件法: 很多时候在边界时,是很容易导出状态关系的地方,也可作为特殊情况带入检查,必要时增加特判。

二.区别

      记得离散课上离散老师好像讲过递归和递推的区别,这里就把迭代一块说了吧。
      (1) 递归:直接或间接的自身调用自身,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,先将现在要求的大问题放在原处不作处理,转而将其转化成几个小问题,在解决完小问题的基础上再回到原处来解决这个大问题,因此递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量,但是时间复杂度却比较高,易超。

      (2)  递推:由前推后,用若干步可重复的简运算或者规律来推下一步。它是按照一定的规律来计算序列中的每个项,通常是通过计算机前面的一些项来得出序列中的指定象的值。

      (3)  迭代:为了逼近所需目标或结果而重复反馈过程的活动。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。

注意:递归递归,递了还要归,是一个往返的过程,处理完小问题还要回来继续处理本身问题,而递推就是仅仅由前推后,非必要情况无需再回。

三.dp递推

       相信大家对斐波那契数列(Fibonacci sequence)一定都很熟悉了,咱们在求F(n)时,总会先求F(n-1)和F(n-2),然后根据公式F(n)=F(n-1)+F(n-2)  (n≥2,n∈N*)求出,特别指出:第0项是0,第1项是第一个1。那么当咱们求出F(3)后,再求F(5)时,难道还要再推回F(1)然后再求一遍F(3)吗?数量小的时候无所畏惧,可数据量大的时候就会超了。其实咱们完全可以在第一遍算出F(3)时就记录下来,以后再需要的时候直接调过来用,无需再重新算一遍了。

      下面我们来举个栗子看一下叭~


第一关——一只小蜜蜂~

Description

      有一只经过训练的蜜蜂只能爬向右侧相邻的蜂房,不能反向爬行。请编程计算蜜蜂从蜂房a爬到蜂房b的可能路线数。 
其中,蜂房的结构如下所示。

 
 

Input

输入数据的第一行是一个整数N,表示测试实例的个数,然后是N 行数据,每行包含两个整数a和b(0<a<b<50)。 

Output

对于每个测试实例,请输出蜜蜂从蜂房a爬到蜂房b的可能路线数,每个实例的输出占一行。 

Sample Input

2
1 2
3 6

Sample Output

1
3

思路:

       很简单的斐波那契数列的变形,开s数组用来记录从开始到当前位置的可能路线数,求a到b的路线数就可以转化成求s[b-a]。数据量为0到50,所以先打表存进s数组,因此从头至尾只打表进行了计算,以后需要直接调用即可。

代码:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

using namespace std;

int main()
{
    long long int s[100],T,i,a,b;
    scanf("%lld",&T);
    s[0]=1;s[1]=1;
    for(i=2;i<51;i++)
        s[i]=s[i-1]+s[i-2];
    for(i=1;i<=T;i++)
    {
        scanf("%lld%lld",&a,&b);
        printf("%lld\n",s[b-a]);
    }
    return 0;
}

第二关——阿牛的牛肉串~

Description

今年的ACM暑期集训队一共有18人,分为6支队伍。其中有一个叫做EOF的队伍,由04级的阿牛、XC以及05级的COY组成。在共同的集训生活中,大家建立了深厚的友谊,阿牛准备做点什么来纪念这段激情燃烧的岁月,想了一想,阿牛从家里拿来了一块上等的牛肉干,准备在上面刻下一个长度为n的只由"E" "O" "F"三种字符组成的字符串(可以只有其中一种或两种字符,但绝对不能有其他字符),阿牛同时禁止在串中出现O相邻的情况,他认为,"OO"看起来就像发怒的眼睛,效果不好。 

你,NEW ACMer,EOF的崇拜者,能帮阿牛算一下一共有多少种满足要求的不同的字符串吗? 

PS: 阿牛还有一个小秘密,就是准备把这个刻有 EOF的牛肉干,作为神秘礼物献给杭电五十周年校庆,可以想象,当校长接过这块牛肉干的时候该有多高兴!这里,请允许我代表杭电的ACMer向阿牛表示感谢! 

再次感谢!

Input

输入数据包含多个测试实例,每个测试实例占一行,由一个整数n组成,(0<n<40)。 

Output

对于每个测试实例,请输出全部的满足要求的涂法,每个实例的输出占一行。 

Sample Input

1
2

Sample Output

3
8

思路:

       开一个dp二维数组dp[i][j],i代表第几个字母,j代表这个字母是什么(1代表E,2代表O,3代表F),dp数组的值就代表到这个时候有多少种类,先初始化第一个字母都设为1,然后打表,由题意可知,E和F属于同一类,O比较特殊就单算。当前字母可以是什么取决于上一个字母是什么,如果当前字母为O,那么上一个字母就不能是O,那么它的种类数就是上一个字母为E和为F的情况的总和。又因为E和F不受限制,所以总和等于上一个字母为E,O,F的情况总和。打表结束后,需要哪个调哪个。

代码:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

using namespace std;

int main()
{
    long long int dp[100][100],n,i;
    dp[1][1]=1;
    dp[1][2]=1;
    dp[1][3]=1;
    for(i=2;i<51;i++){
        dp[i][1]=dp[i-1][1]+dp[i-1][2]+dp[i-1][3];
        dp[i][3]=dp[i-1][1]+dp[i-1][2]+dp[i-1][3];
        dp[i][2]=dp[i-1][1]+dp[i-1][3];
    }
    while(scanf("%lld",&n)!=EOF)
         printf("%lld\n",dp[n][1]+dp[n][2]+dp[n][3]);
    return 0;
}

第三关: LELE的RPG难题~

Description

人称“AC女之杀手”的超级偶像LELE最近忽然玩起了深沉,这可急坏了众多“Cole”(LELE的粉丝,即"可乐"),经过多方打探,某资深Cole终于知道了原因,原来,LELE最近研究起了著名的RPG难题: 

有排成一行的n个方格,用红(Red)、粉(Pink)、绿(Green)三色涂每个格子,每格涂一色,要求任何相邻的方格不能同色,且首尾两格也不同色.求全部的满足要求的涂法. 

以上就是著名的RPG难题. 

如果你是Cole,我想你一定会想尽办法帮助LELE解决这个问题的;如果不是,看在众多漂亮的痛不欲生的Cole女的面子上,你也不会袖手旁观吧? 
 

Input

输入数据包含多个测试实例,每个测试实例占一行,由一个整数N组成,(0<n<=50)。 

Output

对于每个测试实例,请输出全部的满足要求的涂法,每个实例的输出占一行。 

Sample Input

1
2

Sample Output

3
6

思路:

       本题和上一题类似,但还要加上首尾不同色问题。由题意我们都知道第一个位置涂R,P,G三种颜色均可,便于同色处理我们只讨论第一个是R的情况,然后将结果乘3就是最终答案。dp数组代表当进行到第几个位置的时候,在所有的可能中,该位置有多少种情况为对应的颜色(1代表R,2代表P,3代表G),于是在第一个位置时,将R数量初始化为1,其余两种初始化为0。然后也是打表先存起来便于以后直接调用,因为每相邻两个位置的颜色都不能相同,所以第i个位置的可能涂某颜色的情况就等于前一个位置涂其他两种颜色的情况的总和。然后关于首尾不同色问题,比如当第一个位置涂R时,末尾处的另外两种颜色加和就是最后结果,将末尾处R颜色的情况全部忽略即可。

代码:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

using namespace std;

int main()
{
    long long int dp[100][100],n,i;
    dp[1][1]=1;
    dp[1][2]=0;
    dp[1][3]=0;
    for(i=2; i<51; i++)
    {
        dp[i][1]=dp[i-1][2]+dp[i-1][3];
        dp[i][2]=dp[i-1][1]+dp[i-1][3];
        dp[i][3]=dp[i-1][1]+dp[i-1][2];
    }
    while(scanf("%lld",&n)!=EOF)
    {
        if(n==1)
            printf("3\n");
        else
            printf("%lld\n",3*(dp[n][2]+dp[n][3]));
    }

    return 0;
}

第四关——考新郎 ~

Description

国庆期间,省城HZ刚刚举行了一场盛大的集体婚礼,为了使婚礼进行的丰富一些,司仪临时想出了有一个有意思的节目,叫做"考新郎",具体的操作是这样的: 
(这个题的配图太可恶了.....而且身为一个女生还要做考新郎的题.....而且我还没对象....)

首先,给每位新娘打扮得几乎一模一样,并盖上大大的红盖头随机坐成一排; 
然后,让各位新郎寻找自己的新娘.每人只准找一个,并且不允许多人找一个. 
最后,揭开盖头,如果找错了对象就要当众跪搓衣板... 

看来做新郎也不是容易的事情... 

假设一共有N对新婚夫妇,其中有M个新郎找错了新娘,求发生这种情况一共有多少种可能. 

Input

输入数据的第一行是一个整数C,表示测试实例的个数,然后是C行数据,每行包含两个整数N和M(1<M<=N<=20)。 

Output

对于每个测试实例,请输出一共有多少种发生这种情况的可能,每个实例的输出占一行。 

Sample Input

2
2 2
3 2

Sample Output

1
3

思路:

本题重点就在于错排公式:dp[i]=(i-1)*(dp[i-1]+dp[i-2]),注意dp[1]=0,dp[2]=1,还要注意错排只是排了m个错的人有多少种错的可能,但是人还没有确定下来是哪n个人,因此还要用排列组合求一下C几几,忘记公式的可以看图:

formula

注意还可以用一个技巧,就是在计算阶乘的时候可以直接把n!/(n-m)!算出来,再除以m!来降低复杂度,然后计算最后的结果的时候也可以先除后乘以防止超限。

代码:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

using namespace std;

int main()
{
    long long int dp[100],n,m,s,k,i,c,z;
    dp[1]=0;dp[2]=1;
    for(i=3;i<50;i++){
        dp[i]=(i-1)*(dp[i-1]+dp[i-2]);
    }
    scanf("%lld",&c);
    for(z=1;z<=c;z++)
    {
        s=1;k=1;
        scanf("%lld%lld",&n,&m);
        for(i=n;i>n-m;i--)
            s*=i;
        for(i=1;i<=m;i++)
            k*=i;
        printf("%lld\n",s/k*dp[m]);
    }
    return 0;
}

第五关——折线分割平面~

Description

我们看到过很多直线分割平面的题目,今天的这个题目稍微有些变化,我们要求的是n条折线分割平面的最大数目。比如,一条折线可以将平面分成两部分,两条折线最多可以将平面分成7部分,具体如下所示。 

Input

输入数据的第一行是一个整数C,表示测试实例的个数,然后是C 行数据,每行包含一个整数n(0<n<=10000),表示折线的数量。 
 

Output

对于每个测试实例,请输出平面的最大分割数,每个实例的输出占一行。 
 

Sample Input

2
1
2

Sample Output

2
7

思路一:

       每次画的折线都是由两条线组成,第一条线多i个平面,第二条多i+1个平面。

代码一:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

int main()
{
    long long int dp[10001],T,i,n;
    dp[1]=2;
    for(i=2;i<=10000;i++)
        dp[i]=dp[i-1]+4*i-3;
    scanf("%lld",&T);
    while(T--)
    {
        scanf("%lld",&n);
        printf("%lld\n",dp[n]);
    }
    return 0;
}

思路二:

       直接找规律:2,7,16,29,46,67,92,121  ............ 2*n*n-n+1

代码二:

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#include<string.h>

int main()
{
    long long int T,n;
    scanf("%lld",&T);
    while(T--)
    {
        scanf("%lld",&n);
        printf("%lld\n",2*n*n-n+1);
    }
    return 0;
}

妈耶............ 太太太太坑了叭..............  总算弄完了哈哈~

欢迎访问https://blog.csdn.net/lxt_Lucia~~

宇宙第一小仙女\(^o^)/~~萌量爆表求带飞=≡Σ((( つ^o^)つ~dalao们点个关注呗~~

猜你喜欢

转载自blog.csdn.net/lxt_lucia/article/details/81100723
今日推荐