DP问题从入门到精通3(区间DP,计数DP)

Posted 芜湖之肌肉金轮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DP问题从入门到精通3(区间DP,计数DP)相关的知识,希望对你有一定的参考价值。

DP问题从入门到精通系列

有时候dp问题真的不能光看,看是真的看不出答案的,要自己动手去写去研究才会有结果

 

区间DP

区间dp也是dp问题的一个经典分类,这类题目的dp方程通常是二维的且较好理解


设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

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

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2堆,代价为 99,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2,32 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价


 还是老样子,一见到dp题目我们就要开始去思考这类题目的状态表示是什么?

根据题意我们可以这么定义我们的状态方程,把从 i 到 j 合并到一堆的最小代价是多少?

紧接着我就要开始去思考如何状态计算也就是怎么去划分我们刚才所定义的状态方程的这个集合。

我们把集合从i 到 j 划分成若干等分,我们想要求把从 i 到 j 合并到一堆的最小代价,就可以转化为求 i 到 k,和 k+1 到 j 合并的最小代价

我们从 i 到 j 遍历 k就可以求出 i 到 k,和 k+1 到 j 合并的最小代价,从而求出从 i 到 j 合并到一堆的最小代价:

const int N = 310;

int n;
int a[N];
int f[N][N];

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        a[i]+=a[i-1];
    }
    for(int len=1;len<=n;len++)
        for(int i=1;i+len<=n;i++)
        {
            int j=i+len;
            f[i][j]=1e9;
            for(int k=i;k<j;k++)
            {
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+a[j]-a[i-1]);
            }
        }
    cout<<f[1][n];
    return 0;
}

 最后答案就是我们状态表示所说的f[ 1 ][ n ],即从 1 到 n 合并到一堆的最小代价  

整数划分

上一题是属于区间dp的,而这道题则是计数dp(属性对应的是数量而非max或min)


一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示总划分数量。

由于答案可能很大,输出结果请对 109+7 取模。


还是老样子,我们需要去先思考这道题状态表示是什么,我们可以很快的发现,这题其实和完全背包有点像,比如 4 = 1+1+2。我们可以认为是选了1 1 2 体积等于4的这样一个背包,那么这道题能不能用完全背包(背包问题)来做呢?

 

答案是可以的

 

我们可以表示成从前 i 个数中选,和恰好为 j 的方案数。

然后状态计算可以让用完全背包的取划分:

接着既然是完全背包,我们就可以用一维去优化

const int N = 1010;

int f[N];

int n;
int main()
{
    cin>>n;
    f[0]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=n;j++)
        if(j>=i)
         f[j]=(f[j]+f[j-i])%(1000000007);
    cout<<f[n];
    return 0;
}

 

 那么还有没有别的办法呢?其实一道题的状态方程不一定是唯一,因为你对一道题的思考角度不一样,你的状态表示就不一样,你的状态方程也就不一样:


我们可以这么想,把这个表示成总和为 i 组成目标的数恰好为 j 的集合。

 

我们可以分割成这两个集合,那么这两个集合 分别是什么意思呢?

 

第一个集合,是最小值为1的集合,假如一个最小值为1的集合减去1,那么总和也减1,组成目标数的数量 j 也减1(因为最小值1被减去了)

 

第二个集合,是最小值大于1的集合,假如所有组成目标数的数都大于1,那么我们给每个数减去1,组成目标数的数量 j 不变,总和 i - j,

 

因为这两个集合一个是最小值为 1 一个是最小值大于 1 ,所以可以说明其包含了所有的情况,不漏。

const int N = 1010;

int f[N][N];

int n;
int main()
{
    cin>>n;
    f[0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
         f[i][j]=(f[i-1][j-1]+f[i-j][j])%(1000000007);
   
   int res=0;
   for(int i=1;i<=n;i++)res=(res+f[n][i])%(1000000007);
   cout<<res;
    return 0;
}

区间DP,计数DP结束

dp真的越学越上瘾,希望上面的一些学习心得可以帮助到大家(学习网站acwing) 

 

以上是关于DP问题从入门到精通3(区间DP,计数DP)的主要内容,如果未能解决你的问题,请参考以下文章

DP问题从入门到精通3(区间DP,计数DP)

DP问题从入门到精通2.2(线性DP,最短编辑距离)

DP问题从入门到精通2.2(线性DP,最短编辑距离)

DP问题从入门到精通2.2(线性DP,最短编辑距离)

DP问题从入门到精通1(背包问题)

DP从入门到精通2.1(线性DP,上升子序列,公共子序列)