zoukankan      html  css  js  c++  java
  • 闫氏DP分析法

    yxc老师bilibili原视频地址

    众所周知,OI界有一种神奇(ex)的算法,它几乎可以应用在任何题上,它的分析让许多OIer感到头皮发麻。它就是:DP

    闫氏DP分析法

    核心思想:从集合的角度考虑

    (color {red}{“所有的DP问题,本质上都是有限集中的最值问题” — yxc})

    阶段

    动态规划有两个要点:状态与状态转移

    那么阶段自然也应该有两个:状态表示状态计算

    状态表示:化零为整

    把几个具有相同点的元素合在一起考虑,成为一个状态

    对于一个状态 (F(i)) ,考虑两个角度:

    • 1.集合(F(i)) 表示什么集合

    由于 (F(i)) 表示的是一堆东西(这也是DP优于枚举的核心),我们要考虑这一堆东西的共同特征,如:所有满足某个条件的元素集合

    这一点请仔细考虑,到底是大于等于,大于,小于,小于等于,等于......这些的不同会导致状态计算方式的不同

    • 2.属性(F(i)) 存的数与集合的关系:如 (max,min,count,sum)

    很明显,(F(i)) 大多数时候是一个数,代表这个集合的某一个属性,多是最大值、最小值、数量、总和等。题目问什么,属性一般就是什么

    状态计算:化整为零

    先看 (F(i)) 表示的集合:

    将其划分为若干个子集,要求不重(针对涉及加和类型的属性)和不漏

    划分的依据:找最后一个不同点(这个待会会讲)

    划分过后,求 (F(i)) 就可根据子集来求

    如:当属性为 (max) 时,(F(i)=max( ext{子集的max} ))
    当属性为 (count) 时,(F(i)=sum_ ( ext{子集的count}))


    具体例子:

    0-1背包问题

    (N) 件物品以及一个容量为 (V) 的背包,每个物品只能使用一次。放入第 (i) 件物体的代价是 (C_i) ,得到的价值是 (W_i)。求在不超过容量的情况下获得的最大价值。

    输入格式:

    第1行两个正整数,分别表示 (N)(V)

    接下来N行分别有 (2) 个正整数 (C_i)(W_i)

    输出格式:

    一行一个正整数,为最大价值总和。

    样例
    #in:
    4 20
    8 5
    9 6
    5 7
    2 3
    #out:
    16
    

    数据范围: (1≤N≤100,1≤V≤10^6,1≤C_i≤10000,1≤W_i≤10000)

    解析:

    根据乘法原理,总共的方案为 (2^n) 。在所有的方案数中选择一个价值最大的方案,属于有限集的最优问题,可以用试着DP来解。

    状态表示

    对于(F(i,j))

    集合:所有只考虑前(i)个物品,且总体积不超过的(j)的方案

    属性:题目要求我们求最大价值,则其属性就是(max)

    状态计算

    首先看一下(F(i,j))集合:

    我们试着把这个集合分成若干个子集

    找最后一个不同点

    对于这个题,最后一个不同点就是最后一个物品选或不选

    所以对于 (F(i,j)) 对应的集合可以如下划分

    显然,这种划分方案不重不漏。

    现在要分别求出左边和右边的最大值。

    • 对于左边的集合,由于它在 (F(i,j)) 内,且不包含 (i) ,那它其实相当于
      (F(i-1,j))

    • 对于右边的集合,我们再次细分,将 (i) 单独拿出来,得到(F(i-1,j-V_i))
      也就是说求右边的最大值:(max(F(i-1,j-V_i)+W_i))

    但是,右边这个集合不一定存在,所以要特判:(j≥V_i)

    于是我们可以得到状态转移方程:

    [F(i,j)=max( F(i-1,j) , F(i-1,j-V_i)+W_i ) ]

    这就事朴素DP的分析过程了,至于压维等时空优化从状态转移方程出发

    (color{red}{“DP的所有优化,都是对代码的恒等变形” — yxc})

    对于01背包,方程中 (F(i,j)) 只与 (F(i-1,x)) 有关,且 (xleq j)

    所以第 (i) 维可以使用滚动数组滚掉。

    还是放一下代码:

    #include<bits/stdc++.h>
    using namespace std;
    int ci[4000],wi[4000];
    int f[138800]//下标要>=c;
    int main()
    {
        int n,c;
        cin>>n>>c;
        int i,j;
        for(i=1;i<=n;i++)
        {
            cin>>ci[i]>>wi[i];
        }
        for(i=1;i<=n;i++)
        {
            for(j=c;j>=ci[i];j--)
            {
                f[j]=max(f[j],f[j-ci[i]]+wi[i]);//左子集就是f[j]=f[j],省略没写
            }
        }
        cout<<f[c];
        return 0;
    
    }
    

    完全背包问题

    (N) 种物品以及一个容量为 (V) 的背包,每种物品有无限个可用。放入第 (i) 种物体的代价是 (C_i) ,得到的价值是 (W_i)。求在不超过容量的情况下获得的最大价值。

    输入格式:

    第1行两个正整数,分别表示 (N)(V)

    接下来N行分别有 (2) 个正整数 (C_i)(W_i)

    输出格式:

    一行一个正整数,为最大价值总和。

    解析

    仍然从两个角度考虑:

    设状态 (F(i,j))

    状态表示:

    对于 (F(i,j)):

    集合:所有只从前(i)个物品中选,总体积不超过(j)的所有方案。

    属性:(max)

    原因和01背包相似,毕竟都是背包问题

    状态计算:

    对于 (F(i,j)) 的集合:

    划分子集

    这里,最后一个物品可以选若干个,所以要把集合划分成若干个,分别代表不选第 (i) 种,选(1)(i),选两个(i)......

    不重不漏性显然。

    考虑每个子集怎么求。

    第一个子集:不选第(i)种物品:显然,就是 (F(i-1,j))

    剩下的子集,我们可以试着考虑一般情况:选(k)个第(i)种物品。

    那么每个方案可以再次细分为两个部分:(k)个第(i)种物品和前面 (i-1) 种物品总体积 (j-kV_i) 的方案

    所以最大值就是:(F(i,j)=max( F(i-1,j-kV-i)+kW_i)

    于是易得状态转移方程:

    [F(i,j)=max( F(i-1,j) , F(i-1,j-V_i)+W_i , F(i-1,j-2V_i)+2W_i , dots) ]

    但是这个东西项数太多,我们平时写的只有两项啊?

    那想想办法把它转换成两项

    由上面的状态转移方程我们可以得到:

    [F(i,j-V_i)=max( F(i-1,j-V_i) , F(i-1,j-2V_i)+W_i , F(i-1,j-3V_i)+2W_i , dots) ]

    观察得到,上面的每一项,都是下面的每一项加上一个 (W_i) 。类比,上面的最大值就是下面的最大值加上一个 (W_i)

    于是我们可以得到我们最常用的状态转移方程:

    [F(i,j)=max( F(i-1,j) , F(i,j-V_i)+W_i ) ]

    得到了状态转移方程,我们就要来想想能否有时空优化了。

    对比一下01背包和完全背包的状态转移方程

    01:(F(i,j)=max( F(i-1,j) , F(i-1,j-V_i)+W_i ))
    完全:(F(i,j)=max( F(i-1,j) , F(i,j-V_i)+W_i ))

    只有一个(i-1)(i)的不同,但就是这一个不同,致使在滚动数组时一个是从大到小枚举一个是从小到大枚举。

    放一下参考code:

    #include <bits/stdc++.h>
    using namespace std;
    int ci[10010],wi[10010];
    int f[100010];
    int main()
    {
    	int t,n;
    	scanf("%d%d",&t,&n);
    	for(int i=1;i<=n;i++)
    		scanf("%d%d",&ci[i],&wi[i]);
    	for(int i=1;i<=n;i++)
    	{
    		for(int j=ci[i];j<=t;j++)
    		{
    			f[j]=max(f[j],f[j-ci[i]]+wi[i]);
    		}
    	}
    	cout<<f[t];
    	return 0;
    }
    

    石子合并

    设有(N)堆石子排成一排,其编号为 (1,2,3,dots ,N)

    每堆石子都有一定的质量,可以用一个整数来描述,现在要把这 (N) 堆石子合并成一堆。

    每次只能合并相邻的两堆,合并的代价是这两堆石子的质量之和,合并后与这两堆石子相邻的石子将要和新的堆相邻。

    显然,合并时的顺序不同会导致合并的总代价不同。

    找出一种合理的方法,使得将所有石子合并成一堆的代价最小,输出最小代价。

    输入格式:

    第一行一个整数(N)表示石子的堆数。

    接下来的一行(N)个整数,第(i)个整数表示第&i&堆石子的质量。

    输出格式:

    一个整数,表示代价。

    数据范围:(1leq Nleq 100)

    样例:
    #in:
    4
    1 3 5 2
    #out
    22
    

    解析

    满足有限集最优化请读者自证

    仍然从两个角度考虑:

    状态表示

    对于(F(i,j))

    集合:所有将区间([i,j])合并成一堆的方案集合

    属性:题目求的是最小值,所以(min)

    状态计算

    老套路,来看这个(F(i,j))表示的集合:

    仍然是考虑如何划分这个集合

    考虑最后一个不同点

    最后一次, 也就是合并到([i,j])时,一定是由两个区间([i,k])([k,j])合并而来的。显然,(kin [i,j])

    所以我们考虑以这个分界点 (k) 为划分依据,分成 (j-i) 类。

    再来看一下合并的区间:

    两个区间互不干扰,所以两边取(min),两边恰好是 (F(i,k))(F(k+1,j))

    但是这只是两个子区间的最小代价,求 (F(i,j)) 还要加上这部分石子的总质量

    于是(F(i,j) = min( F(i,k+1) + F(k+1,j) ) + S_j-S_{i-1})(S)是石子重量的前缀和。

    放代码:

    #include <bits/stdc++.h>
    using namespace std;
    
    const int N=310;
    
    int n;
    int s[N];
    int dp[N][N];
    
    int main()
    {
    	cin>>n;
    	for(int i=1;i<=n;i++)
    	{
    		cin>>s[i];
    		s[i]+=s[i-1];
    	}
    	if(n==1)//n=1的情况特判 
    	{
    		cout<<0;
    		return 0;
    	}
    	for(int l=2;l<=n;l++)//枚举区间长度 
    	{
    		for(int i=1;i+l-1<=n;i++)
    		{
    			int j=i+l-1;
    			dp[i][j]=0x3f3f3f3f;
    			for(int k=i;k<j;k++)
    			{
    				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
    			}
    		}
    	}
    	cout<<dp[1][n];
    	return 0;
     } 
    

    练习

    留一道题练练手

    如果找不到感觉,可以再看看上面的过程,yxc老师的视频里也有讲这道题。

    最长公共子序列

    给定两个长度分别为 (N)(M) 的字符串 (A)(B),求(A,B)最长公共子序列长度。

    输入格式

    第一行两个整数(N,M)

    第二行为一个长度为(N)的字符串,表示字符串 (A)

    第三行为一个长度为(M)的字符串,表示字符串 (B)

    字符串均由小写字母构成

    输出格式

    一个整数,表示最大长度。

    样例
    #in
    4 5
    acbd
    abedc
    
    #out
    3
    


    总结

    DP是一个经验性的问题,闫氏DP分析法只是给出了一个分析角度,帮助人更好的分析考虑DP的有关问题,里面的状态表示设计仍然需要做题经验的积累才能顺利完成。道阻且长,但是经验多了自然水到渠成。

  • 相关阅读:
    uitableview中文排序问题
    跳转到指定页面popToViewController用法
    [__NSCFConstantString size]: unrecognized selector sent to instance 错误
    [NSThread sleepForTimeInterval:3.0];
    XCode 4.3 Unable to load persistent store UserDictionary.sqlite 以及 ios simulator failed to install the application
    ios 6.0模拟器页面调出pop窗口消失后无法使用键盘
    iOS真机测试,为Provisioning添加设备
    [IOS笔记]
    UITableView 滚动时使用reloaddata出现 crash'-[__NSCFArray objectAtIndex:]: index (1) beyond bounds (0)' Crash
    C# 自定义用户控件
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/13635809.html
Copyright © 2011-2022 走看看