全天动态规划入门到入坑。。。

一、总概:

  动态规划是指解最优化问题的一类算法,考察方式灵活,也常是NOIP难题级别。先明确动态规划里的一些概念:

  状态:可看做用动态规划求解问题时操作的对象。

  边界条件:不需要、或不能由别的状态推出,且已知、或可算出的状态。递推时就用边界条件推出所有状态。

  状态转移方程:由已知状态推出未知状态所用的方式、或原则等,依照它可用已知状态推出未知状态。

  动态规划(DP)主要有:线性DP,数位DP,区间DP,树形DP,状压DP和其他DP(难度较高的还有数据结构优化DP,博弈论DP,概率(期望)DP),NOIP考察概率见下图:

  

  (长者的销魂字迹~~~)

************************

  动态规划的实现方式一般有三种:“顺着推”,“倒着推”,和记忆化搜索。

  顺着推即算出某一状态后,考虑该状态会影响那些状态,并更新一下它影响的状态;

  倒着推即知道该状态是由受哪些状态影响的,并由那些状态来推出该状态;

  记忆化搜索即运用搜索与递归的思想,算某状态时,就递归算所有影响它的状态,同时为了防止一模一样的状态被计算多次,可以把每次计算出来的状态都存储下来,下次要算它的时候直接返回值就行了。

   核心代码(斐波那契数列的倒着推与顺着推)

int main()
{
cin >> n;
f[]=;
f[]=; for (int a=;a<=n;a++)
{
f[a]=f[a-]+f[a-];
} cout << f[n] << endl; cin >> n;
f[]=;
f[]=;
for (int a=;a<n;a++)
{
f[a+] += f[a];
f[a+] += f[a];
} cout << f[n] << endl;
}

    记忆化搜索:

 

 int dfs(int n)
{
if (n==) return ;
if (n==) return ;
if (suan_le_mei[n]) return f[n]; suan_le_mei[n] = true;
f[n] = dfs(n-) + dfs(n-); return f[n];
}// O(n) int main()
{
cin >> n;
cout << dfs(n) << endl; return ;
}

二、数位DP:

  一般按照数字的位数划分状态,状态一般由多维数组(条件数与维数成正比)表示。由高位开始向低位填数,枚举下一位数字填什么转移状态。

  核心:下一位填什么

  例题:

   1、用DP的方式求解区间[l,r]中数的个数

    题解:

    显然答案为l-r+1,但这样就太没意思了。考虑用DP的方法做。题目等价于l<=v<=r,求v的个数。v的位数一定小于等于r的位数、大于等于l的位数。因此只考虑从v的最高位开始向下填数就好了。众所周知,我们比较数的大小都是将2数整数部分最低位对其后,再从最高位(位数不足的当做该位为0)开始依次比较,除非相等,则第一次得到两数位的数的大小关系就是我们要比较的两数的大小关系。故知当我们填到v的第i位时(从高往低数),v的前i-1位一定小于等于r的前i-1位、大于等于l的前i-1位。考虑v与r的大小关系,知当前i-1位一样时与不同时填第i位的方式也有差异,这是又要考虑v与l的大小关系。要考虑好多,怎么优化呢?

    其实可用前缀和的思想。区间[l,r]中的数的个数等于[0,r]的数的个数-[0,l-1]的数的个数。便可建函数work(0,x)来算出区间[0,x]中数的个数。这样就只需考虑v与上限x的大小关系了。由上面思考得到主要知道两个量(当前所填位、二数前面位数的大小关系),可设数组f[i][2]表示状态,其中i表示当前已填到(这里为从低往高数)第i位,二维下标1表示高位v==x,0表示当前位与高位上v<x,f表示相应时刻时的填数方案数,最后结果为f[1][0]+f[1][1];因为是从第n(x的最高位数)位开始填,边界条件就是f[n+1][1]=1;填第i位数受到高位数的两种大小情况的影响,得状态转移方程:f[i][0]=f[i+1][1]*z[n]+f[i+1][0]*10;f[i][1]=f[i+1][1];(z[n]为x的第n位数。)

    最后输出work(0,r)-work(0,l-1)即得最终答案。(代码提示:因为要用2次work函数,因此别忘清零)

   2、求区间[l,r]中相邻二数位的数的差至少为2的数的个数。

    题解:多了一个条件,多一个维度就完事了。设状态f[i][2][k],k表示当前填了数k。其余思想跟上题无大区别。只有填数时在加一个是否与上个k差2的特判就行。

   3、求区间[l,r]中所有数的各个数位的数之和。

    题解:设二维数组f和g,f的意义同第一题,g[i]表示已填到第i位(i是从低位向高位数,填数时是从高位向低位填数)时所有数的数位之和。当在第i位填下一个数c时,相当于前面f[i+1]个数的每个数后面都填上了一个c,所以总共加了(f[i+1][0]+f[i+1][1])*c。故知g的状态转移方程:g[i][0]=g[i+1][0]*10+f[i+1][0]*(0+1+…+9)+g[i+1][1]*z[n]+f[i+1][1]*(0+1+…+(z[n]-1));g[i][1]=g[i+1][1]+f[i+1][i]*z[n];

   核心代码:

  

 //提前说明一下该代码中状态的二维下标0与1的意义与题解是相反的。老师的代码还是抱着敬畏之心看吧(滑稽)
int solve(int x)
{
int n=;
while (x)
{
z[n] = x%;
x/=;
n++;
}
n--; memset(f,,sizeof(f));
memset(g,,sizeof(g)); f[n+][] = ;
g[n+][] = ; for (int a=n;a>=;a--)
for (int b=;b<=;b++)
{
if (b==)
{
for (int c=;c<=;c++)
{
f[a][] += f[a+][b];
g[a][] += g[a+][b] + f[a+][b] * c;
}
}
else
{
for (int c=;c<=z[a];c++)
{
if (c==z[a])
{
f[a][] += f[a+][b];
g[a][] += g[a+][b] + f[a+][b] * c;
}
else
{
f[a][] += f[a+][b];
g[a][] += g[a+][b] + f[a+][b] * c;
}
}
}
} return g[][] + g[][];
}

   4、求[l,r]中所有数的数位的乘积(数据规模<=1018

    题解:如果建三维状态的话第三维开1018个,嗯,内存真香~~。说正事,显然会爆。分析发现乘积的质因子只有2、3、5、7,因为这是10以内数的所有质因子。所以可设状态为六维数组,后四维分别表示2、3、5、7的指数,别看维数这么多,实际上第3维不会超过log21018~=64,而后面的维数更少,反而比一开始的三维状态少了成千上万倍。

      这教给我们,维数越多不一定越大,可能还是优化

三、树形DP

   在树形结构上进行动态规划。明显看出每个节点可由其儿子节点推出,边界条件即叶节点,状态转移方程看情况写。

   例题:

   1、

     给出一个树和若干询问,询问以某个节点为根的子树有多少个节点。

     题解:以一个点为根的子树节点个数等于这个点左、右子树的节点数的个数+1(它本身)。故知状态为以某个节点为根的子树的节点数,边界条件为叶节点(1),状态转移方程即为当前状态=所有儿子状态的和+1;

    代码:

  

 #include<iostream>

 using namespace std;

 int f[];

 void dfs(int p)
{
for (x is p's son) //教学计划还没到存图,这里写伪代码知道意思就好
{
dfs(x);
f[p] += f[x];
}
f[p] ++;
} int main()
{
cin >> n;
read_tree(); dfs();
cout << f[] << endl; return ;
}

   2、

     树的任意两点都是连通的,树的直径即整个树中所有连通两点的不经过重复边的路径的最大长度。给出一个数,求它的直径。

     题解:找找路径的特点,发现连接两点的路径都是先从一点向上走走到两点的最近公共祖先然后再向下折连到终点。路径可被该祖先分成有不严格上升关系(大于等于)的两部分。若将此路径看做是由该祖先发出连向两点的路径,则这两条路径的长度和等于一开始的路径。故求树的直径,可看做求某点发出的两条向下路径的总长度最大值。

      如何求一点向下发出的两条总长度最大的路径呢?一点向下发出路径,必定经过它的儿子。因此我们可以从下往上推,设f[i][0]为i节点向下发出的最长的一条路径,f[i][1]为次长的一条路径。边界条件为叶节点的f都为0。当我们推到i时,i的儿子必已经推完。易由父子关系得f[i][0]=max(f[son1][0],f[son2][0],...,f[sonk][0])+1(假设有k个儿子),设f[u][0]是其中最大的,则f[i][1]=max(f[sona1][0],f[sona2][0],...,f[sonak-1][0])(a取遍1到k中不为u的所有数)。至于怎么从下往上推,可利用DFS的回溯。最后答案即枚举所有点i,找到最大的f[i][0]+f[i][1]。

四、区间DP

    当一个问题具有如下性质:1.合并相邻的两个元素;2.最终合并成一堆,求最优化的代价。便可以用区间DP。区间DP一般设状态为二维的数组f[i][j],其中i为区间左端点,j为区间右端点。

     普通思路:转移状态是枚举两区间中断部分。

   例题:P1880 [NOI1995]石子合并https://www.luogu.org/problemnew/show/P1880

    

    题解:以求最小值为例:

      先看不为环的情况。设石子为a1,a2,...,an,每次合并石子只会改变石子整体性,不改变石子顺序。只要求将若干相邻石子合并,而没有其他条件,可设状态f[i][j],表示把原第i堆石子到原第j堆石子全部合并最小的得分,边界条件为a[i][i]=0(i=1,2,...,n)。考虑得到f[i][j]的方式,最近一次合并前石子ai+ai+1,...,aj一定是两堆石子ai+ai+1,...,ap和ap+1,ap+2,...,aj。枚举每一个p,得到的最小得分即为f[i][j],这就是状态转移方程。(易错警告)枚举的顺序必须保证用已经推出来的量去推出没有推出来的量,所以应先把所有区间长度为2的f求出来,再求长度为3,4,...,n。不能只是简单双双从1到n枚举i和j。因为是要求最小值,所以应有初始化成大数的操作(这里用0x3f初始化是因为首先0x3f3f3f3f很大,而且两个加起来不会爆int,但如果2个0x7ffffff加起来的话就爆int,随后自然大多爆零了)。那么最后的答案即为f[1][n]。

      如果为环呢?发现即使为环,在合并的过程中也有一个石子的分界一直没有被破坏,或换个角度,将环割成一条线,只要枚举分割的地方,最后在取所有分割得到的答案的最小值就行了。

     对于最大值,把最小改成最大就行了。

      时间复杂度:O(N3),空间:二倍的数组空间。

    代码:

    

 #include<iostream>

 using namespace std;

 const int INF=0x3f3f3f3f;

 int main()
{
cin >>n;
for (int a=;a<=n;a++)
cin >> z[a];
memset(f,0x3f,sizeof(f));//求最小,注意往大里初始化。用Ox7f初始化后二元素相加会
//int溢出——爆零,而用0x3f初始化后二个元素相加也没事。
for (int a=;a<=n;a++)
f[a][a] = ; for (int len=;len<=n;len++)//(易错警告)注意枚举的顺序,要确保用已经知道的状态推出来现状态
for (int l=,r=len; r<=n; l++,r++)
for (int p=l;p<r;p++)
f[l][r] = min(f[l][r],f[l][p]+f[p+][r]+sum[l][r]); cout << f[][n] << endl; return ;
}

五、状压DP
    

    例题:已知平面上有一些点及它们的坐标,求从第一个点开始经过其他所有点的路径的最短长度。(英文TSP问题,中文旅行商问题,也是个NP-hard问题,即最小的时间复杂度为O(2n))

    题解:第i个元素是否在已经到过的集合里用一个二进制数第i位上的0或1表示。设f[i][j],i为表示集合的十进制数,j为当前走到的节点,f为最短长度。

    见代码:

    

 #include<cstdio>
#include<iostream> using namespace std; const int maxn=; int n; double f[<<maxn][maxn];//表示集合的二进制数最多有n位,最高位为第n-1位(从0开始),转换成十进制后是2^n-1,但数组也是从0开始。 double dis(int i,int j)//算距离
{
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
} int main()
{
cin >> n;
for (int a=;a<n;a++)
cin >> x[a] >> y[a]; for (int a=;a<(<<n);a++)//手打初始化
for (int b=;b<n;b++)
f[a][b] = 1e+; f[][] = ;//二进制下最低位从零开始。 for (int s=;s<(<<n);s++)
for (int i=;i<n;i++)
if ( ((s>>i) & ) == )//i已经到过
for (int j=;j<n;j++)
if ( ((s>>j) & ) == )//j没有到过
f[s|(<<j)][j] = min(f[s|(<<j)][j] , f[s][i] + dis(i,j)); double ans = 1e+;
for (int a=;a<n;a++)
ans = min (ans , f[(<<n)-][a]); cout << ans <<endl; }

    时间复杂度:空间复杂度:

    n<=20还是能接受的。

  总结一下各数据规模一般对应的时间复杂度:

    n<=12:O(n!) 大暴力

    n<=20:O(2n*n2) NP-hard 问题

    n<=32     n<=50    高深算法优化,还是算了吧

    n<=100 O(n3)小暴力、Floyd

    n<=1000 O(n2) 简单的暴力,各种算法

    n<=100000  O(n log n)数据结构题、算法优化题

    n<=1000000 O(n) 线性算法(线性筛等)

    n>1000000 O(log n) 或 O(1)

    普及一下:计算机运行速度一般每秒3*108~109语句吧

六、其他DP(线性DP,背包问题)

   线性DP:最长……子序列 长度

  https://www.cnblogs.com/InductiveSorting-QYF/p/10402806.html

    可见这篇博客,有优化算法。

   背包问题:

    背包九讲:https://blog.csdn.net/yandaoqiusheng/article/details/84782655(真香)

   强化版数字金字塔:求路径%k的最大值(k<=100)

      题解:多条件,加维度,在普通数字金字塔的状态下添第三维度:模k后的值

       也叫我们分析数据范围:为什么数据范围这么小呢?

              1、出题人懒,懒得加强数据

              大多是因为大了做不了

(完结撒花~~~日后发现有什么缺的就不定日更新呵呵~)

<知识整理>2019清北学堂提高储备D3的更多相关文章

  1. <知识整理>2019清北学堂提高储备D2

    简单数据结构: 一.二叉搜索树 1.前置技能: n/1+n/2+……+n/n=O(n log n)  (本天复杂度常涉及) 2.入门题引入: N<=100000. 这里多了一个删除的操作,因此要 ...

  2. <知识整理>2019清北学堂提高储备D4

    今天主要讲一下数学的知识. 一.进制转换: 十进制到k进制:短除法:顺除至0,逆序取余. k进制转十进制:乘权相加. 常见进制:四进制(对应2位二进制).八进制(对应3位二进制).十六进制(对应4位二 ...

  3. <知识整理>2019清北学堂提高储备D1

    一.枚举: 枚举是最简单最基础的算法,核心思想是将可能的结果都列举出来并判断是否是解. 优点:思维简单,帮助理解问题.找规律.没头绪时 缺点:时空复杂度较高,会有很多冗余的非解(简单的枚举几乎没有利用 ...

  4. <知识整理>2019清北学堂提高储备D5

    今天主讲图论. 前言:图的定义:图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),E称为边集(Edges set),E与V不相交.它们亦可写成V(G)和E(G). 一.图的存 ...

  5. 清北学堂提高突破营游记day1

    上午7点半到的国防宾馆,8点开始的培训. 讲课人林永迪. 没错就是这个人: 他推荐的教辅:刘汝佳紫书,算法导论(也就看看..),刘汝佳白书 先讲模拟.(貌似就是看题论题. 然后贪心. 贪心没有固定的模 ...

  6. 清北学堂提高组突破营游记day3

    讲课人更换成dms. 真的今天快把我们逼疯了.. 今天主攻数据结构, 基本上看完我博客能理解个大概把, 1.LCA 安利之前个人博客链接.之前自己学过QWQ. 2.st表.同上. 3.字符串哈希.同上 ...

  7. 清北学堂提高组突破营考试T1

    题目如下: (想要作弊的后几届神仙们我劝你们还是别黈了,这个题如果你们不会只能证明你们上错班了). 好,题目看完了,发现是一道大模拟(%你)题,于是我们按照题目说的做: #include<ios ...

  8. 清北学堂提高组突破营游记day6

    还有一天就结束了..QWQ 好快啊. 昨天没讲完的博弈论DP: 一个标准的博弈论dp,一般问的是是否先手赢. 博弈论最关键的问题:dp过程. 对于一个问题,一定有很多状态,每个状态可以转移到其他的一些 ...

  9. 清北学堂提高组突破营游记day5

    长者zhx来啦.. (又要送冰红茶了...) zhx一上来就讲动态规划...是不是要逼死人.... 动态规划: 最简单的例子:斐波那契数列.因为他是递推(通项公式不算)的,所以前面的已经确定的项不会影 ...

随机推荐

  1. linux 命令之重定向

    linux 重定向及部分命令 一,重定向讲解: 1> 标准输出重定向 覆盖原有内容 慎用!!!!!! 1>> 标准输出追加重定向 追加内容 2> 错误输出重定向 只输出错误信息 ...

  2. GreenPlum 大数据平台--并行备份(四)

    01,并行备份(gp_dump) 1) GP同时备份Master和所有活动的Segment实例 2) 备份消耗的时间与系统中实例的数量没有关系 3) 在Master主机上备份所有DDL文件和GP相关的 ...

  3. Python 的 __new__()方法与实例化

    __new__() 是新式类中才有的方法,它执行在构造方法创建实例之前.可以这么理解,在 Python 中类中的构造方法 __init__() 负责将类实例化,而在 __init__() 启动之前,_ ...

  4. cxf 框架 webservice

    cxf 内置了一个web服务器 cxf简单入门实例 package test; import org.apache.cxf.jaxws.JaxWsServerFactoryBean; import c ...

  5. 重入锁--ReentrantLock

    本部分主要参考<java并发编程艺术>一书相关内容,同时参考https://blog.csdn.net/zhilinboke/article/details/83104597,说的非常形象 ...

  6. BNU29376——沙漠之旅——————【技巧题】

    沙漠之旅 Time Limit: 1000ms Memory Limit: 65536KB 64-bit integer IO format: %lld      Java class name: M ...

  7. [转]Asp.net Core 使用Redis存储Session

    本文转自:http://www.cnblogs.com/hantianwei/p/5723959.html 前言 Asp.net Core 改变了之前的封闭,现在开源且开放,下面我们来用Redis存储 ...

  8. JavaScript获取url参数

    声明:以下内容转自网络 方法一 String.prototype.getUrlString = function(name) { var reg = new RegExp("(^|& ...

  9. js之正则表达式基础

    字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在.比如判断一个字符串是否是合法的Email地址,虽然可以编程提取@前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦, ...

  10. pdf转为html查看pdf.js

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...