五一 DAY 3

 DAY 3      2019.4.30


动态规划DP

Dp是一个很抽象的东西

方法没有明显区别,很难总结套路

啥是DP?

DP等价于DAG!!!

(1)无后效性:DP的所有状态之间组成一个DAG

(2)最优子序列

(3)阶段性

(4)转移方程:如何计算状态

                          一般是顺序转移

                          有时候乱序,因为DP 是DAG,可以拓扑排序,然后从头for一遍

(5)状态:题目要算的东西

前置知识

简单的例子

斐波那契数列 

0   1  1  2  3  5  8  13 

         F0=0          //边界条件

         F1=1          //边界条件

   Fn=Fn-1+Fn-2     // 转移方程    

定义域,f0到正无穷

数列有一个边界条件,F0,F1      

规定好的条件叫边界条件f0 , f1 , 不依赖于其他feibo数的值,直接求出值

f0,f1,.....fn 叫做状态,dp要求的未知东西,根据边界条件求出,依赖转移方程

对于DP,确定边界条件,转移方程,状态

F0,F1,.....Fn     //状态

F0=0          //边界条件

F1=1          //边界条件

Fn=Fn-1+Fn-2     // 转移方程

 

 

(我们来看这个题)

以斐波那契数列为例,求斐波那契数列第n项 

数组f[n]  表示 feibo第n 项

      其他值加起来求 f[a] ,

      用到再算,别人更新自己

#include<cstdio>
#include<iostream>

using namespace std;

int n,f[233];

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

 

假设 f[a] 已经求出,考虑 f[a] 会影响到哪个值

自己更新别人

#include<cstdio>
#include<iostream>

using namespace std;

int n,f[233];

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

 

Dfs搜索求出feibo

O(f[n]) 代码复杂度和feibo第n项大小一样

斐波那契数列通项公式

后项小于一

O与Feibo前项有关,指数级别复杂度,太慢

斐波那数列的第n项接近2^n

慢的原因:重复调用,每次产生一个新数都要多次计算已经算过的项

记忆化搜索:

记忆化可以解决的,递推或递归一定可以解决

记忆化搜索要多开一个数组,判断是否搜索过,空间多了一点

(矩阵加速实现logn)

保证每一项只被算一次, O(n)

#include<iostream>

using namespace std;

int f[n];

bool suan_le_mei[n];   //判断是否算过 

int dfs(int n)
{
    if (n==0) return 0;       
    if (n==1) return 1;
if (suan_le_mei[n]) return f[n]; //如果已经算过了,那就不算了,直接返回这个值 suan_le_mei[n] = true; //否则标记已算过 f[n] = dfs(n-1) + dfs(n-2); //计算 return f[n]; }// O(n) int main() { cin >> n; cout << dfs(n) << endl; return 0; }

 

介绍4个有套路的

 


 

 

 

显然   Ans= r-l+1

 

考虑用数位DP

 我们设 [ l , r ] 表示区间 l 到 r 中有多少个数字  (前缀和)

首先问题就有一步转化

  [ l , r ] = [ 0 , r ] - [ 0 , l - 1 ]

那么问题的关键就在于求 [ 0 , x ] 

十进制表示 x ,那么它的各位数字分别为

     xn      xn-1      xn-2    .....       x0

从最高位一位一位求[0,x]  (区间有多少个数字)

等价于找到v,使得0≤v≤x

求v的个数

十进制表示 v ,那么它的各位数字分别为

   vn     Vn-1      Vn-2    .....       V0

 V可能有前导0,但任然满足0≤v≤x

     xn      xn-1      xn-2    .....       x0

     vn     Vn-1      Vn-2    .....       V0

在第二行的每一位填上1-9的数,保证0≤v≤x

求v的方案数

填数:从高位开始,从n+1位置转移到n位

那么填Vn-3的时候, vn     Vn-1      Vn-2 已经填

保证 vn     Vn-1      Vn-2 <=  xn      xn-1      xn-2  (v前三位 ≤  x前三位)

希望填了Vn-3之后仍然满足0≤v≤x,前四位≤前四位

分类讨论:

(1) vn     Vn-1      Vn-2   <  xn      xn-1      xn-2  

          此时 Vn-3 填 0--9 任意数

(2) vn     Vn-1      Vn-2   =  xn      xn-1      xn-2  

         此时 Vn-3 填 0-- xn-3  任意数

 f [ i ] [ j ] 由 f [ i - 1 ] [ j ’ ] 确定

先解释一下处理前导0

     0   xn      xn-1      xn-2    .....       x0

     0   vn     Vn-1      Vn-2    .....       V0

填完n+1位后,X,V,相等的方案数有1种,f[n+1][1] = 1;

#include<iostream>

using namespace std;

int l,r,z[233];    //存储每一位 

int f[23333][2];

int solve(int x)   //solve(i),表示0到i有多少个数
{
    int n=0;
    while (x)
    {
        z[n] = x%10;
        x/=10;
        n++;
    }
    n--;
    
    memset(f,0,sizeof(f));   //由于要两次DP,所以开始前清空数组

    f[n+1][1] = 1;     //边界条件,处理前导零的状态

    for (int a=n;a>=0;a--)   // 从头开始填
        for (int b=0;b<=1;b++)  //分类讨论
        {
            if (b==0)
            {
                for (int c=0;c<=9;c++)
                    f[a][0] += f[a+1][b]; //保证始终v<x,方案数直接传递,
                                          //c可以有很多选择,每次成立一次就加一遍上个状态
            }
            else
            {
                for (int c=0;c<=z[a];c++)
                {
                    if (c==z[a])  //填a位的时候方案就转移到了f[a][0]
                                  //因为c可以填好几个数,每填一次就+=上一次的状态
                        f[a][1] += f[a+1][b];
                        
                    else 
                        f[a][0] += f[a+1][b];
                }
            }
        }
        
    return f[0][0] + f[0][1];    // 各位填好后v<=x的方案总数
}

int main()
{
    cin >> l >> r;
    
    cout << solve(r) - solve(l-1) << endl;
    
    return 0;
}

      G[ ][ ]表示数位之和

#include<iostream>

using namespace std;

int l,r,z[233];    //存储每一位 

int f[23333][2];
int g[23333][2];

int solve(int x)   //solve(i),表示0到i有多少个数
{
    int n=0;
    while (x)
    {
        z[n] = x%10;
        x/=10;
        n++;
    }
    n--;
    
    memset(f,0,sizeof(f));   //由于要两次DP,所以开始前清空数组
    memset(g,0,sizeof(g));
    
    f[n+1][1] = 1;     //边界条件,处理前导零的状态
    g[n+1][1] = 0;
    
    for (int a=n;a>=0;a--)        // 从头开始填
        for (int b=0;b<=1;b++)   //分类讨论 
        {
            if (b==0)
            {
                for (int c=0;c<=9;c++)
                {
                    f[a][0] += f[a+1][b];
                    g[a][0] += g[a+1][b] + f[a+1][b] * c;   //拆分数字之和,a+1位和a位(每个方案数贡献一个)
                }
            }
            else
            {
                for (int c=0;c<=z[a];c++)    
                {
                    if (c==z[a]) 
                    {
                        f[a][1] += f[a+1][b];
                        g[a][1] += g[a+1][b] + f[a+1][b] * c;
                    }
                    else 
                    {
                        f[a][0] += f[a+1][b];
                        g[a][0] += g[a+1][b] + f[a+1][b] * c;
                    }
                }
            }
        }
        
    return g[0][0] + g[0][1];  //填完个位之后,统计所有 v <= x 的数位之和
}

int main()
{
    cin >> l >> r;
    
    cout << solve(r) - solve(l-1) << endl;
    
    return 0;
}

比如:135 合法   123非法

题目中有几个条件,就可以列一个多少维度的状态

这个题比之前多了一个条件,多一维状态

 

 推荐题目:

洛谷 Windy数

纠正:

十年以前的省选题,到今天只是NOIP难度

上岁数的省选题不一定是省选难度

k可以填0---9

由于k!=11

所以可以质因数分解

 k = 2* 3* 5* 7d

so,可以把K拆成四个维度,降低复杂度

复杂度   a+b+c+d  : log21018+ log31018+ log51018+ log101018


 

显然是n,

考虑DP

f[i]表示以i为根的子树中有多少个点

边界条件 f[leaf]=1

树形dp 由下至上一点点算上来

DFS 实现节点i子树有多少个点

 

对于每个点,枚举所有儿子,加起来子树之和,再加上自己的1个点

转移方程:

f[i]=f[son1]+f[son2]+.....f[sonn]+1

伪代码如下:

#include<iostream>

using namespace std;

int f[233];

void read_tree()    //存图 

void dfs(int p)      //DFS 实现节点i子树有多少个点
{
    for (x is p's son)
    {
        dfs(x);
        f[p] += f[x];
    }
    f[p] ++;
}

int main()
{
    cin >> n;
    read_tree();
    
    dfs(1);
    cout << f[1] << endl;
    
    return 0;
}

 

直径:在树上找到两个点使得他们距离最远,这个距离就叫做直径

 直径为三

提示:考虑一棵子树内部直径如何求

 

从一个点向下走两条路,合起来这两条路,就是一条路径

要找:

从一个点向下走最长的路径,和次长路径,合起来,对应这个拐点的最大路径

f[i][0]  i向下最长路长度

f[i][1]  i向下次长路长度

举例

最长路f[p][0]=max( f[p1][0] , f[p2][0] ....  f[pn][0] )+1

次长路

f[p][1]=max( f[p1][0] , f[p2][1] ....  f[pn][0] )+1

这样就错了,会出现

 

在找最长路的时候经过了p2,次长路的时候重复了p2,显然这是不可以的

所以直接删掉p2就完了

f[p][1]=max{ f[p1][0] , f[p3][0] ....  f[pn][0] } +1

得到:

最长路      f[p][0]=max( f[p1][0] , f[p2][0] ....  f[pn][0] )+1

次长路      f[p][1]=max{ f[p1][0] , f[p3][0] ....  f[pn][0] } +1

最后枚举每一个点,每个点作为拐点,得到直径就是f[i][0]+f[i][1]

取出最大值

for( 1<= i <=n )

     max{  f[i][0]+f[i][1] }

  推荐题目:

P3304 [SDOI2013]直径


区间dp满足条件:

  1. 合并相邻的东西
  2. 把所有东西合并

 

         F[l][r]     第 l 堆式子到第 r 堆式子合并为一堆的最小代价

边界条件:   f[i][i]=0

最后一次合并,左右合并,两堆合成一堆

整个合并过程中没有改变石头顺序

所以两堆石头存在一个分界线,把它分成左右两堆

 显然要找到一个分界点(断点)p,然后分开左右两边

f[l][p]  f[p+1][r]

左右分开做,互不影响

所以枚举一下分界线(断点)p

最后结果就是:

f [ l ] [ r ] = min ( f [ l ] [ p]  + f[ p + 1 ] [ r ] + sum [ l ] [ r ]  )

f[l][p]      合并断点左边

f[p+1][r] 合并断点右边

sum[l][r] 合并这两堆(此处维护一个前缀和,更加方便)

[代码]:

由于求最小值,初始化最大 0x3f

最好不用0x7f,两个0x7f加起来就爆int

 考虑如何循环

枚举左端点,右端点,断点

但这是错的

在求f[l][r]是没有计算f[l][r],但却需要他来更新

应该是小区间转移到大区间

 

枚举区间长度,按长度,从小到大

O(n3

#include<iostream>

using namespace std;

const int INF=0x3f3f3f3f;

int main()
{
    cin >>n;
    for (int a=1;a<=n;a++)
        cin >> z[a];
    memset(f,0x3f,sizeof(f));  //由于求最小值,初始化最大 0x3f 

    
    for (int a=1;a<=n;a++)
        f[a][a] = 0;  //边界条件
    
    for (int len=2;len<=n;len++)
        for (int l=1,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+1][r]+sum[l][r]);  //
        
    cout << f[1][n] << endl;
    
    return 0;
}

P1880 [NOI1995]石子合并

上一题a1 an 不相邻,本题a1 an相邻,

所以要补区间,扩成两倍,但还是只做了一遍DP

 f[1][n]是考虑a1  an 不相邻

 f[2][n+1]是考虑a1  an相邻,但是a2  a1 不相邻

所以考虑一遍各种情况,每次都会有一个断开的位置,找出断开哪个位置使得结果最小

 推荐题目:

P1063 能量项链


给出点的坐标

(x1,y1)(x2,y2)(x3,y3)……(xn,yn

 

有一个人一开始在一号点

出发,把其余的点都走至少一遍,使得路径最短

这样也是可以的

这叫TSP问题

(旅行商问题)

经典的NP-hard问题   最快也至少是O(2n),n在指数上

(continue)

结论:

最优情况下,每个点只去一次

 

DP每一步枚举所有可能的情况

目前停留在5,它可以有三种情况的转移,因为还有三个点没走

DP转移:

希望把集合变成下标

目前保持的状态是集合,要求压成数

构造一个n位二进制数

每个元素只有可能在或者不在集合里

在 1

不在 0

所以说这个二进制数与集合{1,4,5}等价

S,已经走过的点的集合

F[ s ][ i ]从起点出发,走过集合s里的点,停留在i,所走过的最小距离

初始化f[1][1]=0

转移:考虑怎么走

转移

枚举j点,如果没走过它,我们把他加进去s试试

 相应的最短距离要更新

转移方程

 

把s的第j位从0变成1

如果从i走到j比从起点到j近

#include<cstdio>
#include<iostream>

using namespace std;

const int maxn=20;

int n;

double f[1<<maxn][maxn];   //F[2^n][n]这么大就好

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=0;a<n;a++)   //a下标 0-n-1
        cin >> x[a] >> y[a];
        
    for (int a=0;a<(1<<n);a++)   //手动初始化数组
        for (int b=0;b<n;b++)
            f[a][b] = 1e+20;     //要求最小值,手动无穷大
            
    f[1][0] = 0;   //初始化边界 
    
    for (int s=1;s<(1<<n);s++)   //枚举S
        for (int i=0;i<n;i++)    //枚举最后停留在那个点,停在i点
            if ( ((s>>i) & 1) == 1)   // 看S二进制第i位是0还是1, 保证i在集合 s里
             for (int j=0;j<n;j++)   // 枚举没走过的点 (j位一定为0)
                 if ( ((s>>j) & 1) == 0)  //s>>j,那么i位就到了第0位
                     f[s|(1<<j)][j] = min(f[s|(1<<j)][j] , f[s][i] + dis(i,j));//转移方程
                     
    double ans = 1e+20;
    for (int a=1;a<n;a++)   //不可能停在起点0号,所以从1枚举 
        ans = min (ans , f[(1<<n)-1][a]);  //2的N次方减一就是N个位都是1 ,也就是都走过了一遍点
        
    cout << ans <<endl;
        
}

它的时间复杂度是O(n^2 * s^n)
空间复杂度O(2^n * n)
(一秒钟的运算量在3*10^8~10^9之间)
差不多n<=20

补充一些复杂度有关的知识

n<=12(O(n!)的复杂度,直接暴搜)
n<=22 或20(状压O(n^2 * s^n))
n<=32 (直接放弃)
n<=50 (直接放弃)
n<=100 (O(n^3))
n<=1000 (O(n^2))
n<=10^5 (数据结构 O(nlogn))
n<=10^6 (线性算法 O(n))

n>10^6(O(1))


2019.7.17又讲一遍

  1. 平面上N个点,第i个点坐标(xi,yi),从1号点出发,在平面上任意走,求走遍所有点再回来的最短路径

使得距离最短,一个点没必要走两次,每次从没走过的点选一个出来

f[s][i]  i 在i点,s走过了哪些点

状态压缩:把一个数组压缩成一个数

 

自己更新别人

所以我们要找没走过的点,枚举j,看看s的第j位二进制位是不是0

最后答案看看停在那个点,最后还要回来

#include<iostream>

using namespace std;

double f[][];
double x[233],y[233];

int main()
{
    cin >> n;
    for (int a=0;a<n;a++)
        cin >> x[a] >> y[a];
    f=∞
    f[1][0]=0;
    for (int s=0;s<(1<<n);s++)
        for (int i=0;i<n;i++)
            if (f[s][i] < ∞)
            {
                for (int j=0;j<n;j++)
                    if ( ((s>>j) & 1) == 0)
                    {
                        int news = s | (1<<j); //加上这个数字 
                        f[news][j] = min(f[news][j],f[s][i] + dis(i,j));
                    }
            }
    for (int i=0;i<n;i++)
        ans=min(ans, f[(1<<n)-1][i] + dis(i,0));
        
    return 0;
}

O(2^n  *  n^2)

N<=22或者N<=20

一定是先枚举s,然后s从小到大,因为走过的点是越来越多的

 推荐题目:

P1171 售货员的难题


题目每多一个条件就多一个维度(至少保留暴力分。。)

 

Fij    从起点走到 i,j 的方案数

每个坐标都可以向右或下走,除了边界,每个点由左边的点或上边的点转移过来

边界只能由一个方向转移过来

边界条件:f1j=fi1=1

转移方程:fij=fi-1,j+fi,j-1

其实也就是这样

Cn-1n+m-2

 

数字三角形

希望走过的路径最大

变化量:走的过程中,位置变化

f[i][j] 走到(i,j)时走过路径的最大值

可以从左上角或者正上方转移过来

转移方程:f[ i ][ j ] = max( f[ i-1 ][ j ] , f[ i-1 ][ j-1 ] ) + a[ i ][ j ]

Joyoi

http://www.joyoi.cn/

 数字三角形2

PS: 实在不行加一维

f[i][j] 走到(i,j) %100 后的最大值

错误

前面的最优不一定保证后面最优

 

比如你现在在98 99中选择,99一定最优,但是下一步再选,就会降为hin小的数

维度不够加一维

所以考虑加一维度

bool  f[i][j][k]表示走到[i][j]时的路径和%100=k是否可行

#include<iostream>

using namespace std;

bool f[233][233][233];  //bool数组 

int main()
{
    cin >> n;
    for (int i=1;i<=n;i++)
        for (int j=1;j<=i;j++)
            cin >> a[i][j];
            
    f[1][1][a[1][1] % 100] = true;  //初始化 
    for (int i=1;i<n;i++)
        for (int j=1;j<=i;j++)
            for (int k=0;k<100;k++)
                if (f[i][j][k])  //判断之前是否可达,否则走就没必要 
                {
                    f[i+1][j][(k+a[i+1][j])%100]=true;
                    f[i+1][j+1][(k+a[i+1][j+1])%100]=true;
                }
    
    for (int j=1;j<=n;j++)   //最后枚举在哪一列 
        for (int k=0;k<100;k++)  
            if (f[n][j][k]) ans=max(ans,k);
    cout << ans << endl;
    
    return 0;
}

最长上升子序列

(和导弹拦截有异曲同工之妙)

数据加强到n<=105

用数据结构 线段树 加速

 我的背包问题

系统的学习详见背包九讲

 好趴下面放3个背包问题:

    

 01背包

例题:采药

N个物品,M个体积

物品i,体积vi,价值wi

头疼:设计状态

放物品,考虑有什么未知量???

第一维:放了多少物品,[i]放好了i个物品

第二维:体积 [j] 已经放进去的物品,体积是多少

F[i][j] 放进i个物品,体积为j的最大价值

放进物品的编号<=i ,每个物品可以放或者不放,j已经放进去i个物品的体积之和

如何转移???

每次都考虑好了前i种物品,那么就考虑第i+1放或者不放

如果不放第i +1个物品,体积不变,价值不变

放了后体积会+vi,价值+wi

 f[ i ][ j ] = max( f[ i-1 ][ j ]  ,  f[ i-1 ][ j-v[i] ] + wi )

外层for循环维度

  状态转移

J要判断啊,不可以超过背包容积

最后答案不一定是f[n][m],因为不一定体积越大价值越大,但是答案一定是考虑了所有n个物品,所以for一遍体积,ans取max

    for (int i=1;i<=n;i++)
        for (int j=0;j<=m;j++)
        {
            f[i][j] = f[i-1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
        
    int ans=0;
    for (int a=0;a<=m;a++)
        ans = max(ans,f[n][a]);
    cout << ans << endl;

 

  无限背包问题

 

从别人转移到自己

放0个:f[i-1][j]

放1个:f[i-1][j-vi]

放2个:f[i-1][j-2*vi]

放k个:f[i-1][j-k*vi]

可以枚举第i个物品到底放了几个

for (int i=1;i<=n;i++)
        for (int j=0;j<=m;j++)
            for (int k=0;k*v[i]<=j;k++)
                f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);

要判断k*v[i]<=j,保证体积不会爆炸

But 3层循环 O(n^3),慢了hin多

如何优化???

其实不需要降维,只需要把01背包改一下

f[ i ][ j ] = max( f[ i ][ j ] , f[ i ][ j-v[i] ] + w[ i ] )

 

之前是由i-1这种i的上一层状态转移过来的

此时就可以同一行的转移,横向转移,转移多少次就相当于一种物品放了多少个

复杂度 : O(nm)

for (int i=1;i<=n;i++)
        for (int j=0;j<=m;j++)
        {
            f[i][j] = f[i-1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    int ans=0;
    for (int a=0;a<=m;a++)
        ans = max(ans,f[n][a]);
    cout << ans << endl;

  有限背包问题

 

直接k枚举多少次 O(n^3)

考虑优化???

比如现在一个物品能放13次

造体积为vi,价值为wi,只能用1次的物品

造体积为2vi,价值为2wi,只能用1次的物品

造体积为4vi,价值为4wi,只能用1次的物品

造体积为6vi,价值为6wi,只能用1次的物品

变成捆绑包的组合

01背包问题

O(n^2  *  k)

如何处理捆绑包大小???

相当于二进制拆分

(1)如果一个数字可以被2^k这样111111分解,那么很显然捆绑包组合起来可以实现所有值

(2)那如果不能111111这样分解,最后一定会剩下一个数字,把这个数字打包成一个捆绑包,也就是你算的时候,先算上这个数字,然后后面就变成一个可以被1111拆分的,就又回到上面情况

拆出来捆绑包个数k≈logn

所以复杂度就是O( nm logn )

只需要读入处理一下

#include<iostream>

using namespace std;

int n,m,w[233],v[233];
int f[233][233];

int main()
{
    cin >> n >> m;
    int cnt = 0;
    for (int a=1;a<=n;a++)
    {
        int v_,w_,z;
        cin >> v_>> w_ >> z;
        
        int x = 1;
        while (x <= z)
        {
            cnt ++;
            v[cnt] = v_*x;
            w[cnt] = w_*x;
            z-=x;
            x*=2;
        }
        if (z>0)  //Z最后有剩余,新建一个捆绑包 
        {
            cnt ++;
            v[cnt] = v_*z;
            w[cnt] = w_*z;
        }
    }
    n=cnt;
    for (int i=1;i<=n;i++)
        for (int j=0;j<=m;j++)
        {
            f[i][j] = f[i-1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    int ans=0;
    for (int a=0;a<=m;a++)
        ans = max(ans,f[n][a]);
    cout << ans << endl;
    return 0;
}

最后是一个无处安放的DP

P4408 [NOI2003]逃学的小孩

以及网址

http://west14.openjudge.cn/

原文地址:https://www.cnblogs.com/xiaoyezi-wink/p/10795613.html