算法讲解之Dynamic Programing —— 区间DP [变形:环形DP]

来源:互联网 发布:深圳万度网络科技开发 编辑:程序博客网 时间:2024/05/16 15:24

对区间DP和其变式环形DP的总结。

首先先来例题。 codevs.cn 1048 石子归并

题目描述 Description

有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。

输入描述 Input Description

第一行一个整数n(n<=100)

第二行n个整数w1,w2...wn  (wi <= 100)

输出描述 Output Description

一个整数表示最小合并代价

样例输入 Sample Input

4

4 1 1 4

样例输出 Sample Output

18

区间DP的模型很显然,特征就是所给的数据是一条链或者一条环(环形DP)。

划分问题:

我们所求区间[1,n]的最小合并代价,而dp[1][n]必然是有两段区间合并而成 dp[1][n] = {dp[1][k]+dp[k+1][n]+sum[l][r]}

标红部分可以用前缀和优化

重写DP方程:dp[1][n] = {dp[1][k]+dp[k+1][n] + sum[n]-sum[0]}  1<=k<n

因为我们不知道[1,n]最终是由哪段合并而来的,所以我们必然要枚举断点k.

我们发现,在计算[1,n]时我们需要已经计算完成了的[1,k][k+1,n]

所以我们在枚举到n之前必须先把其前面的区间计算完成。然后再从n开始反向枚举左端点,比如计算[1,5]区间且断点枚举到3的时候,我们必须已经计算出区间[1,3]和区间[4,5]。区间[1,3]好办,因为我们的r是顺着枚举的,区间[4,5]要计算在前面也好办,内层循环反着枚举。(看代码)

我们归纳出一般性的方程 dp[l][r] = {dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]}  l<=k<=r

状态定义 dp[l][r]  -> 区间l~r合并的最小代价,[状态必须是二维,因为我们要表示一个区间,区间的特征是由左界/右界来表现]


代码演示如下

#include<cstdio>  #include<cstring>  #include<algorithm>  #include<iostream>  using namespace std;  int n,w[101];  int f[101][101];//f[i][j]表示区间[i,j]合并的最小代价   int sum[101] ;//预处理i→j的合并代价 (降低算法复杂度)   void dp()  {      for(int j=2;j<=n;j++)          for(int i=j-1;i>=1;i--)          {              f[i][j]=1e7;//定义一个极大值               for(int k=i;k<j;k++)              {                  f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);   //状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j                            }          }  }  int main()  {      scanf("%d",&n);      for(int i=1;i<=n;i++)      {          scanf("%d",&w[i]);            sum[i]=sum[i-1]+w[i];       }                dp();         printf("%d",f[1][n]);      return 0;  }  

当然,我们还有另一种写法。

描述一个区间的状态,我们也可以用左端点+长度来表示。

当然状态定义还是不变(因为要记录的时候dp[l][l+len]等价于dp[l][r])

只是枚举的方式要改变。

这里就不写全,只给出核心部分。

下面代码的执行逻辑和上面的有点区别,

先算出任意相邻两段的合并代价,然后算任意相邻3段的合并代价。。

因为我们在算3段的时候,例[1,3] 我们已经算出了[1,2],[2,3]的合并代价,所以可以直接递推。

for(int len = 2;len <= n;len++)//长度
{
for(int l = 1;l+len-1<=n;l++)//左端点
{
int r = l+len-1;
for(int k=l;k<r;k++)//断点
{
dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]);
}
}
}

答案是dp[1][n]

区间DP的写法一般就这两种形式,选一种你喜欢的方式记下即可。状态定义一般也是二维[l,r]

好,然后我们再来看下一道例题

题目描述 Description

在一个园形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分.

输入描述 Input Description

数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数.

输出描述 Output Description

输出共2行,第1行为最小得分,第2行为最大得分.

样例输入 Sample Input

4
4 4 5 9

样例输出 Sample Output

43
54

http://codevs.cn/problem/2102/

数据由链变成了环,难道我们就没法处理了?

这里有一种技巧叫做 “断环为链

将链复制一倍 a[i+n] = a[i]

然后我们控制枚举的长度n即可。看代码实现。

最后算答案的时候再枚举一遍起点和固定长度n,dp[l][l+n-1] 1<=l<=n

#include<cstdio>  #include<cstring>  #include<algorithm>  #include<iostream>  using namespace std;  int n,w[101];  int f[201][201];//f[i][j]表示区间[i,j]合并的最小代价   int g[201][201];//f[i][j]表示区间[i,j]合并的最大代价   int sum[201] ;//预处理i→j的合并代价 (降低算法复杂度)   int ans_min=1e7;  int ans_max=0;    void dp()  {      for(int j=2;j<=n+n;j++)          for(int i=j-1;i>=1&&j-i<n;i--)          {              f[i][j]=1e7;//定义一个极大值               for(int k=i;k<j;k++)              {                  f[i][j] = min(f[i][j],f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);   //状态转移方程 f[i][j] = min{f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]} | i <= k < j                  g[i][j] = max(g[i][j],g[i][k] + g[k + 1][j] + sum[j] - sum[i - 1]);                               }              if(ans_max<g[i][j])ans_max=g[i][j];          }  }    int main()  {      scanf("%d",&n);      for(int i=1;i<=n;i++)      {          scanf("%d",&w[i]);            w[i+n]=w[i];      }      for(int i=1;i<=2*n;i++)      {          sum[i]=sum[i-1]+w[i];      }      dp();         for(int i=1;i<=n;i++)          {              if(ans_min>f[i][i+n-1])ans_min=f[i][i+n-1];          }      printf("%d\n",ans_min);      printf("%d\n",ans_max);      return 0;  }  

上面代码是用第一种方式写的,你也很容易用第二种(左端点+长度)的方式写出来。这里就不给出代码了。

啊,顺手就写了一份,好吧,还是把代码贴出来吧。不过希望大家认真思考,DP的题看到代码和方程可能很容易懂,但难度在于想到代码和方程,或者是定义状态。初学者可以先学习,过一段时间后再来做。复习的人就不要看代码啦。

#include<cstdio>#include<iostream>#include<cmath>#include<algorithm>#include<cstring>using namespace std;int n;int w[220],sum[220];int f[220][220];int g[220][220];int main(){scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&w[i]);w[i+n] = w[i];}for(int i=1;i<=2*n;i++)sum[i]=w[i]+sum[i-1];for(int len = 2;len <= n;len++){for(int l=1;l<=2*n-len+1;l++)//这里要注意{int r = l+len-1;f[l][r]=1e9;for(int k=l;k<r;k++){f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);g[l][r] = max(g[l][r],g[l][k]+g[k+1][r]+sum[r]-sum[l-1]);}}}int ans_max = 0,ans_min = 1e9;for(int i=1;i<=n;i++){ans_min = min(ans_min,f[i][i+n-1]);ans_max = max(ans_max,g[i][i+n-1]);}cout<<ans_min<<endl;cout<<ans_max;return 0;}

然后再来一道经典题,能量项链 codevs 1154

题目描述 Description

Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n(Mars单位),新产生的珠子的头标记为m,尾标记为n

需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设N=44颗珠子的头标记与尾标记依次为(23) (35) (510) (102)。我们用记号⊕表示两颗珠子的聚合操作,(jk)表示第jk两颗珠子聚合后所释放的能量。则第41两颗珠子聚合后释放的能量为:

(41)=10*2*3=60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为

((41)2)3=10*2*3+10*3*5+10*5*10=710

输入描述 Input Description

第一行是一个正整数N4N100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1iN),当i<N< span>时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出描述 Output Description

只有一行,是一个正整数EE2.1*109),为一个最优聚合顺序所释放的总能量。

样例输入 Sample Input

4

2 3 5 10

样例输出 Sample Output

710

#include<cstdio>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>using namespace std;int n;int a[220];int dp[220][220];int main(){scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&a[i]);a[i+n] = a[i];}//写法一: /*for(int r=2;r<=2*n;r++){for(int l=r-1;l>=1&&r-l+1<=n;l--){for(int k=l;k<r;k++){dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a[k+1]*a[r+1]);}}}*///写法二: for(int len = 2;len <= n;len++){for(int l = 1;l <= n;l++){int r = l+len-1;for(int k=l;k<r;k++){dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a[k+1]*a[r+1]);}}}int ans = 0;for(int i=1;i<=n;i++)ans = max(ans,dp[i][i+n-1]);cout<<ans;return 0;}

下面的可以选听(DP优化之四边形不等式优化)最后再补几道题可以去练习。

3002 石子归并 3

题目描述 Description

有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。

输入描述 Input Description

第一行一个整数n(n<=3000)

第二行n个整数w1,w2...wn  (wi <= 3000)

输出描述 Output Description

一个整数表示最小合并代价

样例输入 Sample Input

4

4 1 1 4

样例输出 Sample Output

18

数据范围及提示 Data Size & Hint

数据范围相比“石子归并” 扩大了

先看百科http://baike.baidu.com/link?url=H5fTEgnfYzvIRIMrUQaIZW14QAsALYL5ixFsEY-V5gnbsztPZu-OpBiq-0hvaD0hsCqFGaWQ6sA5gmv6xuB6OK

还有必看的《动态规划加速原理之四边形不等式》 http://wenku.baidu.com/link?url=8g7HwSNikz9L3qNWUVa2N7KQCL3N4pwcn8H73oa_IBQP5qfoujx3dNGd-FPjHFV3IXUZDOgrifg4EdCLrJorUvWMNdNZTotYYlMFkn2d3KC

http://wenku.baidu.com/view/78566b87bceb19e8b8f6ba87.html 

http://wenku.baidu.com/view/b9c80f85763231126edb1196.html?re=view

题解http://blog.cloudlunar.com/view/CodeVS/1048

对于2P/1P的动态规划,如果d[i,j]=min{d[i,k-1]+d[k+1,j]}+w[i,j](i<=k<=j),即类似归并石子问题的DP方程,则有

  • 定理1(性质):如果w满足四边形不等式和区间单调关系,d也满足四边形不等式。
  • 定理2(性质):定理1条件满足,设使d[i,j]取最小值的k为K[i,j]那么K[i,j-1]<=K[i,j]<=K[i+1,j]。
  • 定理3(用于证明是否为凸):w为凸<=>w[i,j]+w[i+1,j+1]<=w[i+1,j]+w[i,j+1]。(即只要证明w[i,j+1]-w[i,j]关于i递减即可证明函数为凸)

看不懂没关系,记结论就行了。至于四边形优化,用的范围不广考到的可能性不大?

(还有凸四边形不等式有个记忆法则:“不交叉比交叉更优” or "包含比交叉更优" orz.)

DP方程优化后为:

  1. dp[i,j]=min{dp[i,k-1]+dp[k,j]}+w[i,j](K[i,j-1]<=k<=K[i+1,j])
AC代码

#include<cstdio>#define INF (1<<30) int dp[3010][3010]={},K[3010][3010]={},n,a[3010]={},sum[3010]={};int main(){    scanf("%d",&n);    for (int i=1;i<=n;i++)    {        scanf("%d",&a[i]);        sum[i]=sum[i-1]+a[i];        K[i][i]=i;    }    for (int len=2;len<=n;len++)        for (int i=1;i<=n-len+1;i++)        {            int j=i+len-1;            dp[i][j]=INF;            for (int k=K[i][j-1];k<=K[i+1][j];k++)                if (dp[i][k-1]+dp[k][j]+sum[j]-sum[i-1]<dp[i][j])                {                    dp[i][j]=dp[i][k-1]+dp[k][j]+sum[j]-sum[i-1];                    K[i][j]=k;                }        }    printf("%d",dp[1][n]);    return 0;}

1090 加分二叉树

 

2003年NOIP全国联赛提高组 codevs.cn


题目描述 Description

设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第j个节点的分数为ditree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:

subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数

若某个子树为主,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空

子树。

试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;

1tree的最高加分

2tree的前序遍历

 

 

现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。

输入描述 Input Description

1行:一个整数nn<=30),为节点个数。

2行:n个用空格隔开的整数,为每个节点的分数(分数<=100

输出描述 Output Description

1行:一个整数,为最高加分(结果不会超过4,000,000,000)。

2行:n个用空格隔开的整数,为该树的前序遍历。

样例输入 Sample Input

5

5 7 1 2 10

样例输出 Sample Output

145

3 1 2 4 5

数据范围及提示 Data Size & Hint

nn<=30)

分数<=100

#include<cstdio>#include<iostream>#include<cstring>#include<algorithm>#include<cmath>using namespace std;int n;int a[40];typedef long long ll;ll dp[40][40];int root[40][40];void PreOrder(int l,int r){printf("%d ",root[l][r]);if(l<root[l][r])PreOrder(l,root[l][r]-1);if(r>root[l][r])PreOrder(root[l][r]+1,r);}int main(){scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",&a[i]);for(int i=1;i<=n;i++)dp[i][i]=a[i],root[i][i]=i;for(int len=2;len<=n;len++){for(int i=1;i+len-1<=n;i++){int j = i+len-1;for(int k=i;k<=j;k++){if(k-1<i)dp[i][k-1] = 1;if(k+1>j)dp[k+1][j] = 1;if(dp[i][k-1]*dp[k+1][j]+a[k]>dp[i][j]){dp[i][j] = dp[i][k-1]*dp[k+1][j]+a[k];root[i][j] = k;}}}}printf("%d\n",dp[1][n]);PreOrder(1,n);return 0;}

1166 矩阵取数游戏

 

2007年NOIP全国联赛提高组


题目描述 Description

【问题描述】
帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的n*m 的矩阵,矩阵中的每个元素aij均
为非负整数。游戏规则如下:
1. 每次取数时须从每行各取走一个元素,共n个。m次后取完矩阵所有元素;
2. 每次取走的各个元素只能是该元素所在行的行首或行尾;
3. 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分= 被取走的元素值*2i
其中i 表示第i 次取数(从1 开始编号);
4. 游戏结束总得分为m次取数得分之和。
帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。

输入描述 Input Description

第1行为两个用空格隔开的整数n和m。
第2~n+1 行为n*m矩阵,其中每行有m个用单个空格隔开的非负整数。

输出描述 Output Description

输出 仅包含1 行,为一个整数,即输入矩阵取数后的最大得分。

样例输入 Sample Input

2 3
1 2 3
3 4 2

样例输出 Sample Output

82

数据范围及提示 Data Size & Hint

样例解释

第 1 次:第1 行取行首元素,第2 行取行尾元素,本次得分为1*21+2*21=6
第2 次:两行均取行首元素,本次得分为2*22+3*22=20
第3 次:得分为3*23+4*23=56。总得分为6+20+56=82

 

【限制】
60%的数据满足:1<=n, m<=30, 答案不超过1016
100%的数据满足:1<=n, m<=80, 0<=aij<=1000

分析题目发现行与行之间的结果互不相干,所以空间不需要开三维,二维就行了。

接下来开始考虑思路。不难发现,取走一部分数字后,剩下的数字总是形成一个区间。由于取走的数的个数已知,剩下的数字按顺序的权值也就知道了,由此得出,这是一个区间DP,和石子合并类似。它满足最优子结构,也满足无后效性。

用f[i][j]表示区间[i, j]的最优解,有两种解法

f[i][j] = max(a[i]  + 2 * f[i+1][j], a[j] + 2 * f[i][j-1]);//直接计算数的权值  f[i][j] = max(a[i]*2^(m-j+i) + f[i+1][j], a[j]*2^(m-j+i) + f[i][j-1])//每次翻倍  

代码(并未AC):

#include<cstdio>  #include<cstring>  #include<algorithm>  using namespace std;    const int maxn=80+5;  const int maxl=32;  int n,m;  int a[maxn],f[maxn][maxn],aa[maxn],bb[maxn],ans[maxn];      int main()  {      int ans=0;      scanf("%d%d",&n,&m);//n行m列       for (int i=1;i<=n;i++)      {          for (int j=1;j<=m;j++) scanf("%d",&a[j]);          for (int j=1;j<=m;j++) f[j][j]=a[j];          for (int j=1;j<=m-1;j++)//区间长度           {              for (int k=1;k<=m-j;k++)//起点              {                  int l=k+j;                  f[k][l] = max(a[k]  + 2 * f[k+1][l], a[l] + 2 * f[k][l-1]);              }           }          ans+=2*f[1][m];      }      printf("%d\n",ans);      return 0;  }  
没AC的原因是溢出了。。

题中也明确指出要高精度。

代码不想写,我贴一份AC的代码

#include<cstdio>#include<cstring>#include<algorithm>using namespace std;const int maxn=80+5;int n,m;int a[maxn];//以上是非高精类定义 const int power=4;const int base=10000;const int maxl=10;/**需要用到  *高精加高精(状态转移时数组加数组)  *高精加单精(状态转移时a[k]+f[i][j])   *重载等号 用于把int赋值给结构体   *把f数组开成结构体,每个结构体都是一个高精数  *按普通数组读入,高精处理  *可以尝试一下,先写出来框架 */struct num{    int a[maxl];        num()    {         memset(a,0,sizeof(a));    }        num operator + (const num &b)    {        num c;        c.a[0]=max(a[0],b.a[0]);        for (int i=1;i<=c.a[0];i++)        {            c.a[i]+=a[i]+b.a[i];            c.a[i+1]+=c.a[i]/base;            c.a[i]=c.a[i]%base;        }        if (c.a[c.a[0]+1])++c.a[0];          return c;     }        num operator + (const int &b)//this->都可以省略     {        a[1]+=b;        int i=1;        while (a[i]>=base)        {            a[i+1]+=a[i]/base;            a[i]%=base;            i++;        }        if (a[a[0]+1])a[0]++;          return *this;    }        num operator = (int b)    {        a[0]=0;        while (b)        {            a[0]++;            a[a[0]]=b%base;            b/=base;        }        return *this;    }        bool operator < (const num &b)const//必须加const     {        if (a[0] < b.a[0]) return true;          if (a[0] > b.a[0]) return false;          for (int i = a[0];i > 0;--i)          {              if (a[i] != b.a[i]) return a[i] < b.a[i];          }          return false;      }        void print()    {        printf("%d", a[ a[0] ]);                //先打印最高位,为了压位 或者 该高精度数为0 考虑          for (int i = a[0]-1;i > 0;--i)          printf("%0*d", power, a[i]);            //这里"%0*d", power的意思是,必须输出power位,不够则前面用0补足          printf("\n");     }    }ans,f[maxn][maxn];int main(){     scanf("%d%d",&n,&m);//n行m列     for (int i=1;i<=n;i++)    {        for (int j=1;j<=m;j++) scanf("%d",&a[j]);        for (int j=1;j<=m;j++) f[j][j]=a[j];//如何赋值? 可以尝试重载等号         for (int j=1;j<=m-1;j++)//区间长度         {            for (int k=1;k<=m-j;k++)//起点            {                int l=k+j;                //f[k][l] = max(a[k] + f[k+1][l] + f[k+1][l], a[l] + f[k][l-] + f[k][l-1]);//这样写无法识别 除非在结构体外面写 或者重载前加friend                 f[k][l] = max(f[k+1][l] + f[k+1][l] + a[k], f[k][l-1] + f[k][l-1] + a[l]);//高精加即可 重载一下加号用于高精加单精             }         }        ans = ans + f[1][m];        ans = ans + f[1][m];    }    ans.print();    return 0;} 

2493 GF弹钢琴
题目描述 Description

  PianoEater喜欢听钢琴曲,并且一直梦想着给他的GF Little Pink弹奏一曲。于是PianoEater去钢琴王国大学(Piano Kingdom University,简称PKU)找钢琴十级的rainbow学习弹琴。
  PianoEater弹琴时,他的一只手上的5根手指不能交叉,并且两根手指不能放在同一个琴键上。同时,一只手的跨度不能超过9个白键(大拇指和小指之间最多间隔7个白键)。弹琴时,左右臂可以交叉,但是用(其中一只手的手指)去按(处于另一只手的两个手指之间)的按键是不允许的。

  现在PianoEater有一架有52个白键和36个黑键的钢琴,并且他要弹奏的曲子只需要按白键。在同一时刻,他只用弹奏一个音符。如果这个音符不移动大拇指就可以按到,那么他不需要耗费体力;否则他需要花费sqrt(x)(下取整)的体力来移动手的位置(也就是移动大拇指的位置)。其中x代表移动前后大拇指的位置之差的绝对值。
  现在有一首由N个音符组成的乐曲,每个音符用0~51之间的一个整数表示,分别对应了52个白键。0是最左边的键,51是最右边的键。PianoEater想知道他弹完这首曲子最少需要耗费多少体力。

输入描述 Input Description

  输入的第一行是三个整数,L,R,N,分别表示初始时刻左手大拇指的位置、右手大拇指的位置和乐曲的音符数。
  接下来N行每行一个在0~51之间的整数,代表需要弹奏的音符。

输出描述 Output Description

  输出一个整数,表示最少需要耗费的体力。

样例输入 Sample Input

10 20 10
0
1
2
3
4
5
6
7
8
9

样例输出 Sample Output

2

数据范围及提示 Data Size & Hint

  对于30%的数据,1<=N<=100
  对于50%的数据,1<=N<=500
  对于100%的数据,1<=N<=1000,4<=L<=51,0<=R<=47

样例解释:
  最初左手大拇指在10,第一次花费sqrt(2)=1的体力移动到8,此时0~8这9个键都能被左手够到。最后再花费sqrt(1)=1的体力移动到9即可。


来源:Nescafe 21

首先,我们不需要考虑某个键具体是哪根手指按下的。如果左手大拇指在L,右手大拇指在R,那么在[L-8,L]之间的键就能被左手按到,在[R,R+8]之间的键就能被右手按到。所以我们只需要知道两个手大拇指的位置。

其次,虽然题目中给了手指不能交叉等限制,但是仔细思考可以发现,这些限制都是没用的。因为显然存在一种不交叉的方案比交叉的要优。

所以我们设计出以下动态规划算法:

F[i,j,k]表示弹完第i个音符时,左手大拇指在j位置,右手大拇指在k位置的最小体力花费。

对于第i+1个音符,如果需要按第p个键,那么F[i,j,k] 可以转移到F[i+1,l,k] (p<=l<=p+8)F[i+1,j,r] (p-8<=r<=p),相应的计算花费就可以了。

时间复杂度约为O(18*N*52^2)

代码:

#include<cstdio>#include<iostream>#include<cmath>#include<cstring>#include<algorithm>using namespace std;int L,R,n;int a[1010];int f[1010][60][60];int main(){cin>>L>>R>>n;for(int i=0;i<=n;i++){for(int l=0;l<=51;l++){for(int r=0;r<=51;r++){f[i][l][r] = 1e9;}}}for(int i=1;i<=n;i++)cin>>a[i];f[0][L][R] = 0;for(int i=0;i<=n-1;i++){for(int j=4;j<=51;j++){for(int k=0;k<=47;k++){if(f[i][j][k]<1e9){for(int p=a[i+1];p<=a[i+1]+8;p++){if(p>51)break;f[i+1][p][k] = min(f[i+1][p][k],f[i][j][k]+(int)sqrt(abs(p-j)));}for(int p=a[i+1];p>=a[i+1]-8;p--){if(p<0)break;f[i+1][j][p] = min(f[i+1][j][p],f[i][j][k]+(int)sqrt(abs(k-p)));}}}}}int ans = 1e9;for(int i=4;i<=51;i++){for(int j=0;j<=47;j++){ans = min(ans,f[n][i][j]);}}cout<<ans;return 0;}

慎用memset,作者君开始图方便就直接memset 1e9 然后就挂了,debug了好久。。T_T

2497 Acting Cute (据说是BZOJ的权限题!)

题目描述 Description

  正在rainbow的城堡游玩的freda恰好看见了在地毯上跳舞卖萌的水叮当……于是……
  freda:“呜咕>_< 我也要卖萌T_T!”

  rainbow给了freda N秒的自由活动时间,不过由于刚刚游览城堡有些累了,freda只想花B秒的时间来卖萌,剩下的时间她要在rainbow的城堡里睡个好觉好好休息一下。
  rainbow给这N秒每秒定义了一个值Ui,如果第i秒钟freda在卖萌,那么她可以获得Ui点卖萌指数lala~
  freda开始卖萌后可以随时停止,休息一会儿之后再开始。不过每次freda开始卖萌时,都需要1秒来准备= =,这一秒是不能获得卖萌指数的。当然,freda卖萌和准备的总时间不能超过B。
  更特殊的是,这N秒钟时间是环形的。也就是freda可以从任意时间开始她的自由活动并持续N秒。
  为了使自己表现得比水叮当更萌,现在freda想知道,她最多能获得多少卖萌指数呢?

输入描述 Input Description

  第一行包含两个整数N和B。
  第2~N+1行每行一个整数,其中第i+1行的整数表示Ui。

输出描述 Output Description

  输出一个整数,表示freda可以获得的最大卖萌指数。

样例输入 Sample Input

5 3
2
0
3
1
4

样例输出 Sample Output

6

数据范围及提示 Data Size & Hint

  对于60%的数据,N<=100
  对于100%的数据,0<=B<=N<=3600,0<=Ui<=200000。

样例解释:
  freda选择从第2秒开始她的自由活动,持续N秒(2、3、4、5、1)。第4秒开始准备,第5、1秒卖萌(时间是环形的),获得2+4=6点卖萌指数。


来源:Nescafe 22

这道题和前面的石子归并不一样,并且也不是像石子归并2一样简单的断环为链。

可能有点特殊的状态设计。f[i][j][2] -> 前i段卖萌了j段且当前第i段(0没卖萌1卖萌)

为什么不能用普通做法?首先注意空间,然后这道题有限制条件,并且可以休息会再开始,这个如果用普通的状态设计[l][r]是无法处理的。

DP方程
f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1])
f[i][j][1]=max(f[i-1][j-1][0],f[i-1][j-1][1]+a[i])

这道题隐含有一点资源分配(01背包?)的意思在里面。

某人的题解:(解释一下:他的睡和醒就是卖萌不卖萌)

这道题考察的是如何把环形动规转化为线性动规,还是有一定思维难度的。以前所做的环形拆为线性,是把1~n-1再接到n的后边,而这道题巧妙地赋个初值就能解决。

纯线性的动规方程不难想到:f[i,j,0..1]表示前i个时段,睡了j个时段,其中第i个时段是睡着还是醒着(0为醒,1为睡),则f[i,j,0]=max(f[i-1,j,0],f[i-1,j,1]),f[i,j,1]=max(f[i-1,j-1,0],f[i-1,j-1,1]+U[i])。目标是max(f[n,m,0],f[n,m,1]),边界是f[1,0,0]=0,f[1,1,1]=0,其余均为-maxlongint.

而转化为线性,也就是让睡觉的n和1时段连起来,所以只需要强制n时段和1时段必须都睡着就行,并且这样第一个时段也可以获得能量。目标变为:f[n,m,1],边界变为:f[1,1,1]=u[1],其余均为-maxlongint。用滚动数组优化空间避免MLE。循环时,i层从2~n,j层从0~m,其中注意当m=1时直接输出0,特判一下。

算法看下面的代码:(下面的代码又巧妙的优化了状态,优化空间【滚动数组!】因为我们递推后面的状态只需要上一个位置的状态就行了,前面的冗余信息可以抛开)

还有注意2次初始化和递推的区别。

最后再次提醒初值问题,取max的时候dp数组初值必须赋小,(亲测-1(不够小)和0(不管)要错,为什么?自行思考,(不正确的状态会干扰到后面的计算))拿一组数据去调试。

#include<cstdio>#include<iostream>#include<cmath>#include<cstring>#include<algorithm>using namespace std;int n,m,t,ans;int a[3601];int f[2][3601][2];void dp(){t = 0;for(int i=2;i<=n;i++){t = 1-t;//或 t = (t+1)%2for(int j=0;j<=m;j++){f[t][j][0] = max(f[1-t][j][0],f[1-t][j][1]);if(j)f[t][j][1]=max(f[1-t][j-1][0],f[1-t][j-1][1]+a[i]);} }}void init(){for(int i=0;i<=1;i++){for(int j=0;j<=m;j++){for(int k=0;k<=1;k++){f[i][j][k] = -1e9;}}}}int main(){cin>>n>>m;for(int i=1;i<=n;i++)scanf("%d",&a[i]);if(m<=1){cout<<"0"<<endl;return 0;}init();f[0][0][0] = f[0][1][1] = 0;dp();ans = max(f[t][m][0],f[t][m][1]);init();f[0][1][0]=f[0][1][1]=a[1];dp();ans=max(ans,f[t][m][1]);cout<<ans;return 0;}

1258 关路灯

题目描述 Description

多瑞卡得到了一份有趣而高薪的工作。每天早晨他必须关掉他所在村庄的街灯。所有的街灯都被设置在一条直路的同一侧。

多瑞卡每晚到早晨5点钟都在晚会上,然后他开始关灯。开始时,他站在某一盏路灯的旁边。

每盏灯都有一个给定功率的电灯泡,因为多端卡有着自觉的节能意识,他希望在耗能总数最少的情况下将所有的灯关掉。

多端卡因为太累了,所以只能以1m/s的速度行走。关灯不需要花费额外的时间,因为当他通过时就能将灯关掉。

编写程序,计算在给定路灯设置,灯泡功率以及多端卡的起始位置的情况下关掉所有的灯需耗费的最小能量。

输入描述 Input Description

输入文件的第一行包含一个整数N,2≤N≤1000,表示该村庄路灯的数量。

第二行包含一个整数V,1≤V≤N,表示多瑞卡开始关灯的路灯号码。

接下来的N行中,每行包含两个用空格隔开的整数D和W,用来描述每盏灯的参数,其中0≤D≤1000,0≤W≤1000。D表示该路灯与村庄开始处的距离(用米为单位来表示),W表示灯泡的功率,即在每秒种该灯泡所消耗的能量数。路灯是按顺序给定的。

输出描述 Output Description

输出文件的第一行即唯一的一行应包含一个整数,即消耗能量之和的最小值。注意结果小超过1,000,000,000。

样例输入 Sample Input

4

3

2 2

5 8

6 1

8 7

样例输出 Sample Output

56

#include <iostream>#include <cstring>#define ref(i,x,y)for(int i=x;i<=y;i++)using namespace std;long long n,s,a[1001],w[1001];long long t[1001][1001],f[1001][1001][2];int main(){cin>>n>>s;ref(i,1,n)cin>>a[i]>>w[i];ref(i,1,n)ref(j,i,n)t[i][j]=t[i][j-1]+w[j];int sum=t[1][n];ref(i,1,n)ref(j,i,n)t[i][j]=sum-t[i][j];memset(f,1,sizeof f);f[s][s][0]=f[s][s][1]=0;ref(l,2,n)ref(i,1,n-l+1){int j=i+l-1;f[i][j][0]=min(f[i+1][j][0]+t[i+1][j]*(a[i+1]-a[i]),f[i+1][j][1]+t[i+1][j]*(a[j]-a[i]));f[i][j][1]=min(f[i][j-1][1]+t[i][j-1]*(a[j]-a[j-1]),f[i][j-1][0]+t[i][j-1]*(a[j]-a[i]));}cout<<min(f[1][n][0],f[1][n][1]);return 0;}

为什么要这样写?因为这个题除了要记录已经访问过的区间以外,还要记录从哪里转移过来。


9.4日补,单调队列优化DP.

3327 选择数字

题目描述 Description

给定一行n个非负整数a[1]..a[n]。现在你可以选择其中若干个数,但不能有超过k个连续的数字被选择。你的任务是使得选出的数字的和最大。

 

输入描述 Input Description

第一行两个整数n,k

以下n行,每行一个整数表示a[i]。

输出描述 Output Description

输出一个值表示答案。

样例输入 Sample Input

5 2

1

2

3

4

样例输出 Sample Output

12

数据范围及提示 Data Size & Hint

对于20%的数据,n <= 10

对于另外20%的数据, k = 1

对于60%的数据,n <= 1000

对于100%的数据,1 <= n <= 100000,1 <= k <= n,

                  0 <= 数字大小 <= 1,000,000,000

dp[i] = max(dp[i],dp[j-1]+sum[i]-sum[j]);
看到这道题我们就想到上述形式的DP方程,dp[i]表示考虑到当前第i个的最优解。  i-k<=j<i, j是枚举空格的位置。

sum[i]是用前缀和的方式优化。
然后变形得

DP方程:dp[i] = max(dp[j-1]+sum[i]-sum[j]) i-k<=j<i     =>  dp[i] = max(dp[j-1]-sum[j])+sum[i]    抽象 dp[i] = max(f[j])+sum[i]这种形式的DP方程可以用单调队列优化。 

关于单调队列,一般用于优化1D/1D动态规划。其形式为f[x]=max/min{g(k)}+w[x]这样的方程就可以优化。

先看百科http://baike.baidu.com/link?url=QPZnIjZu9mX8HvClhpg8iI6RBCoO491hICEOcakId2jc0Slqr4wPLWw6zoyUVH-xRzxe0lU9q21N8LBeLenKrK

然后是http://www.cnblogs.com/ka200812/archive/2012/07/11/2585950.html

AC代码:

#include<cstdio>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>using namespace std;int n,k;#define maxn 100001typedef long long ll;int a[maxn];ll dp[maxn],sum[maxn],d[maxn],qq[maxn];int head,tail=1;int main(){cin>>n>>k;for(int i=1;i<=n;i++)scanf("%d",&a[i]);for(int i=1;i<=n;i++){sum[i] = sum[i-1]+a[i];}if(k>=n){cout<<sum[n];return 0;}/*for(int i=1;i<=k;i++)dp[i] = sum[i];for(int i=k+1;i<=n;i++){for(int j=i-1;j>=i-k;j--){dp[i] = max(dp[i],dp[j-1]+sum[i]-sum[j]);}}*/for(int i=1;i<=n;i++){d[i] = dp[i-1]-sum[i];if(i>=k+1&&d[i-k-1]==qq[head])head++;while(tail>head&&d[i]>qq[tail-1])tail--;qq[tail++] = d[i];dp[i] = qq[head]+sum[i];}cout<<dp[n];return 0;}

解读上面的代码: 
原始dp方程 dp[i] = max(dp[i],dp[j-1]+sum[i]-sum[j])
变形得 dp[i] = max(dp[j-1]-sum[j])+sum[i] i-k<=j<i
d[j] = dp[j-1]-sum[j]  d数组维护的就是 max()内的。 
把新的d[j]加入队列. 
队列里面只需要存 d[j]   i-k<=j<i 
所以我们把再之前的d[i-k-1]如果在队列里就扔掉 (head++)
(当然由于我们的tail是tail++,所以最后一个元素实际上是 tail-1的位置。)
所以队列有元素的条件是 tail>head
因为我们是取max,所以我们维护的是单调递减的队列,为什么要维护单调递减的队列?因为当我们新加入的d[j]
比队尾的元素大的时候,后面的元素就没有用了(仔细思考为什么)
所以我们就可以直接把后面的覆盖掉。
然后最后取队首元素(队首是最大值)
通过这种方式我们可以降低一层复杂度,只是在常数上增加了一些。
当然还有其他优化方式比如线段树等,不过在这种情况下,单调队列是最优选择。 

理解上面的决策单调性。 

当然写法还有很多。例如用STL的deque [双端队列]来模拟。

(不会双端队列的可以用两个queue模拟)

#include<cstdio>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#include<queue>using namespace std;int n,k;#define maxn 100001typedef long long ll;int a[maxn];ll dp[maxn],sum[maxn],d[maxn];deque<ll> qq;int main(){cin>>n>>k;for(int i=1;i<=n;i++)scanf("%d",&a[i]);for(int i=1;i<=n;i++){sum[i] = sum[i-1]+a[i];}if(k>=n){cout<<sum[n];return 0;}qq.push_back(0);for(int i=1;i<=n;i++){d[i] = dp[i-1]-sum[i];if(i>=k+1&&d[i-k-1]==qq.front())qq.pop_front();while(!qq.empty()&&d[i]>qq.back())qq.pop_back();qq.push_back(d[i]);dp[i] = qq.front()+sum[i];}cout<<dp[n];return 0;}


上述题目均出自codevs.cn大家可以去做做看。

如果你觉得写得好的话,欢迎收藏(右上方)或点赞(下方): )

2 0
原创粉丝点击