动态规划几类例题的笔记

  蒟蒻乱写一通关于动态规划几类问题的笔记,可能会有错误之处,欢迎指正。html

 

一. 01背包问题算法

  关于这个问题,我以前已经写了不太全面的(比较扯淡的)笔记,就不复述了。数组

  传送门:背包问题学习笔记函数

  补充一下除了01背包、彻底背包、多重背包外,还有一个超大背包问题值得了解。学习

 

二. 最长上升子序列问题(LIS)优化

  题目连接:洛谷oj AT2827 LIS  推荐题解:动态规划——最长上升子序列问题spa

  题目不赘述了,LIS就是最长上升子序列。简单来讲,就是在一串给定的数列a[n]中取出一些数(未必要连续),让它们能单调上升,而且这个数列要最长。.net

  举个例子,对于长度为10的数列“1,9,11,2,10,7,8,9,13,6”,它的LIS就是“1,2,7,8,9,13”,长度为6。设计

  对于这个问题,有两种算法,复杂度分别为O(n2)和O(nlogn)。虽然咱们发现O(n2)的算法是没法AC洛谷的LIS板子题的,可是O(n2)的算法思想仍然有助于咱们理解动态规划。code

O(n2)的经典算法:

  根据动态规划把大问题拆成小问题,分段求解的思路,咱们声明一个数组f[maxn],f[i]表示从1到i中,以a[i]结尾的最长上升子序列的长度。初始时f[i]=1,i∈[1,n]。(初始值其实就是这个序列中只有a[i]时的序列长度,显然为1)。

  能够写出状态转移方程:f[i]=max{f[j]+1}, j∈[1,i-1]且a[j]<a[i];

  怎么理解这个方程呢?就是说,当咱们已经处理完了f[i-1],须要求f[i]时,只须要遍历一遍a[1…i-1],找到全部能成为a[i]前驱的数a[j](即a[i]>a[j]),而后在全部能成为前驱的a[j]中找到f[j]最大的那个就能够了。若是还不理解,能够尝试直接看代码。

  由于代码是写出来便于理解的,我就不写寄存器内联快速读入之类花里胡哨的东西了嘻嘻嘻。

#include <cstdio>
using namespace std;
const int maxn=100000;

int n,a[maxn+5],f[maxn+5];
int result;

int main(){
    scanf("%d",&n);
    for (int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        f[i]=1;
    }
    for (int i=1;i<=n;i++)
        for (int j=1;j<i;j++)
            if (a[i]>a[j]&&f[i]<f[j]+1)
                f[i]=f[j]+1;
    for (int i=1;i<=n;i++)
        if (result<f[i])    result=f[i];
    printf("%d",result);
    return 0;
} 

O(nlogn)的优秀算法:

  若是想优化上面的算法,基于贪心的思想,咱们很容易想到:当x,y∈[1,i-1]时,若f[x]=f[y],a[x]<a[y],显然f[i]=f[x]+1比f[i]=f[y]+1更优,更可能获得答案。

  因此在f[x]必定的状况下,尽可能选择更小的a[x]。按f[x]=k来分类,咱们须要记录的当全部等于k的f[x]中,最小的a[x]。咱们声明一个low[k]来存储这个最小的a[x]。

  这样说可能会有点乱,简单说吧,就是声明一个low[k],存储在[1,i-1]之间,已知的最长上升子序列长度为k的最小的a[x]值。(仍是感受比较复杂,将就理解一下吧)

    low[k]=min{a[x]},f[x]=k;

  能够概括出low[k]的几个性质:

    ①low[x]单调递减增,即low[1]<low[2]<low[3]<low[4]<……<low[n-1]<low[n];

    ②随着处理时间推动,low[x]只会愈来愈小;

  若是不能理解,能够尝试本身写个数列模拟看看。

  有了这两个性质,就能够这样求解:

  声明当前已求出的最长上升子序列的长度为len(初始时为1),当读入一个新元素x:

    ①若x>low[len],则直接把x加入到d的末尾,且len+=1;

    ②不然,在low[x]中二分查找,找到第一个比x小的数low[k],并low[k+1]=x,在这里x<=g[k+1]必定成立。

  易证时间复杂度为O(nlogn)。

  代码中的二分查找我用stl的lower_bound函数代替了,可是不开O2会慢挺多吧……手写二分应该会快,蒟蒻我太懒了orz

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=100000;

int n,len=1;
int a[maxn+5],low[maxn+5];

int main(){
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    low[1]=a[1];
    for (int i=2,j=0;i<=n;i++){
        if (low[len]<a[i])    j=++len;
        else    j=lower_bound(low+1,low+len+1,a[i])-(low+1)+1;    //这里用stl里的lower_bound代替手写的二分查询 
        low[j]=a[i];
    }
    printf("%d",len);
} 

 

 三. 最长公共子序列问题(LCS)

  题目连接:洛谷oj P1439 【模板】最长公共子序列  推荐题解:《挑战程序设计竞赛(第二版)》2.3

  注意,此次是最长公共子序列(LCS)。LCS就是指给定两个数列,两个数列中最长的公共子序列(哇我在说什么废话)。

  举个例子好了,好比下面两个长度分别为6的子序列:

    1 4 9 10 2 6

    2 1 10 2 13 6

  上面两个子序列,它们的LCS就是长度为4的序列: 1 10 2 6 。和LIS同样,子序列是不须要连续的。

  为了解决这个问题,咱们能够尝试这样思考:

  首先,记给定的两个序列为s和t,依旧是根据动态规划分段求解的思想。定义f[i][j]为序列 s1…s和序列 t1…t对应的LCS的长度。

  那么f[i+1][j+1]有三种状况:

    ① si+1=ti+1时,在序列 s1…s和序列 t1…t对应的LCS后面追加si+1(si+1=ti+1);

    ② 继承序列 s1…s和序列 t1…tj+1 对应的LCS;

    ③ 继承序列 s1…si+1 和序列 t1…t对应的LCS;

  f[i][j]为上面三种状况中最大的一个。因此能够写出递推式:

    

  这个递推式能够在O(n2)的时间内被计算出来,f[n][n]是LCS的长度。

相关文章
相关标签/搜索