目录见右上
站内文章欢迎各位转载,但是请注明出处(毕竟码字很辛苦哈)。

主题

朴素的DP石子归并

原题:https://codevs.cn/problem/1048/

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数组f[i,j]表示从第i堆到第j堆的最小代价。


代码如下:

uses math;//懒人不打min,调用数学函数库……

var

w,f:array[0..100,0..100] of longint;

i,j,k,n:longint;


begin

read(n);

for i:=1 to n do

  read(w[i,i]);

for i:=1 to n do

  for j:=i+1 to n do

   w[i,j]:=w[i,j-1]+w[j,j];//初始化,w[i,j]表示第i堆到第j堆的所有石子数


for i:=n-1 downto 1 do//从后往前推

  for j:=i+1 to n do

  begin

   f[i,j]:=maxlongint;//很重要

   for k:=i to j-1 do

    f[i,j]:=min(f[i,k]+f[k+1,j]+w[i,j],f[i,j]);//DP方程

  end;


writeln(f[1,n]);

end.


我们知道其实这一题还有另一种做法。

在上述代码中:

f[i,j]表示从第i堆到第j堆的最小代价,所以动态转移方程为

f[i,j]=min(f[i,k]+f[k+1,j]+w[i,j]),其中1<=i<=n-1,i+1<=j<=n,i<=k<=j-1。

最后的答案为f[1,n],并且我们要先把左端点i从后往前枚举,再把右端点j从前往后枚举,这两个东西不能逆序枚举。至于k怎么枚举,开心就好。

但是如果我们不开心,不喜欢downto,怎么办呢?

我们发现,之所以从n-1开始往前downto,就是因为在我们的状态转移方程中,我们在求f[i,j]时只知道f[i,k],并不知道f[k+1,j](因为我们的左端点还没有求到k)。所以如果我们把左端点i从n-1开始枚举,我们就已经知道f[i,k]和f[k+1,j]了,所以这个时候的DP就是正确的。

于是我们就想到DP的无后效性(为什么和无后效性扯上了关系……)。因为我们在求的当前状态是由上一状态推出来的,那么我们总应该要知道上一状态的所有决策所得到的最优答案。

那么我们就发现了,我们可以枚举区间的长度。

因为求当前长度,需要用比它小的长度的区间来操作。这个时候,我们顺着for,DP就是正确的了。

状态转移方程与第一种方法一样,但是在DP的时候有点小不同。至于具体方法,下一个题就是一个很好的例子。




原题:https://codevs.cn/problem/2102/

2102 石子归并 2

题目描述 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


个人解法:

看到原题时间限制10s……(大雾)估计是打错了,因为1s是绰绰有余的,而且,我的代码只需1ms(我认为应该是数据弱),应该是还有比我程序更快的。

这也是一个经典的区间DP。

对于环状的区间,我们枚举长度也许就会更好。

首先我们从小到大枚举长度,接下来我们从左到右枚举左端点,之后我们就可以计算出右端点。之后再for一遍求最优解。

当然还有一点数据处理。我们可以用两倍的数组来储存起点为i顺时针长度共j-i+1的区间。


代码如下:

uses math;//懒人调用的数学函数库。

var

n,i,j,k,l,minn,maxx:longint;

f1,f2:array[0..100,0..100] of longint;

s,w:array[0..200] of longint;


function num(l,r:longint):longint;//计算l到r的所有石子数

begin

  exit(s[r]-s[l-1]);

end;


begin

read(n);

for i:=1 to n do

  read(w[i]);

for i:=1 to n do

  s[i]:=s[i-1]+w[i];

for i:=n+1 to 2*n do

  s[i]:=s[i-1]+w[i-n];//处理起点为i顺时针长度共j-i+1的区间。


for l:=2 to n do

  for i:=1 to 2*n-l+1 do

  begin

   j:=i+l-1;

   f1[i,j]:=maxlongint;

   for k:=i to j-1 do

   begin

    f2[i,j]:=max(f2[i,j],f2[i,k]+f2[k+1,j]+num(i,j));//两次DP

    f1[i,j]:=min(f1[i,j],f1[i,k]+f1[k+1,j]+num(i,j));

   end;

  end;

minn:=maxlongint;


for i:=1 to n do

begin

  maxx:=max(maxx,f2[i,i+n-1]);

  minn:=min(minn,f1[i,i+n-1]);//不一定是第1堆开始最优

end;


writeln(minn);

writeln(maxx);

end.



原题:https://codevs.cn/problem/3002/

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


个人解法:

诶?这不是和石子归并1一样的吗?

只是数据范围大了些嘛……

先不管那么多,把石子归并1的源码版上去,看看多少分。

多少分呢,50分。

那怎么办呢?

我们很神奇地发现这是区间DP。

处理区间DP时间优化,我们有没有想到神奇的四边形不等式?

于是我们就惊奇地发现这就是朴素的四边形不等式练习题。

至于四边形不等式……可以去看看:

https://baike.baidu.com/link?url=0hs8Ga-APbqEm5QfxfS5V7LmbSnLceG0GUVnhA1S5WgOarcDzl9LP_S6xRgx0CJjNjrBkJ-9TLyQckdIqnBYca(这里面讲的比较清楚)

同样,f[i,j]表示第i堆到第j堆的最小代价。

所以,我们就用s[i,j]表示f[i,j]决策时的k的值。

也就是说,我们在求f[i,j]的时候,是从i到j枚举k,最后找到一个k使得f[i,j]最小。

那么s[i,j]的值就是k。

根据四边形不等式,我们就可以得出:

s[i,j-1]<=s[i,j]<=s[i+1,j]。

所以现在我们就只需要从s[i,j-1]开始枚举k,到s[i+1,j]停止。

接下来我们就考虑怎样DP。

我们发现,我们要用到这一个优化,如果最先枚举左端点i,那么我们就要知道s[i+1,j],所以我们就应该从n-1开始往前枚举i。

我们还可以枚举长度,因为我们要知道的s[i,j-1]与s[i+1,j]长度均比s[i,j]小。

这两种方法在石子归并1和2都已经说过,这里就不再赘述。

于是我们就把时间复杂度O(n^3)降到了O(n^2)。


代码如下:

uses math;//懒人不想打min函数

var

w:array[-2..3020] of int64;

f,s:array[-2..3020,-2..3020] of int64;//f与s均如解析

i,j,k,n,l:longint;


begin

read(n);

for i:=1 to n do

begin

  read(j);

  w[i]:=w[i-1]+j;

end;


fillchar(f,sizeof(f),127);

for i:=1 to n do

begin

  s[i,i]:=i;//这里又是懒人思想……在我们求s[i,j]的时候需要用s[i,j-1]。第一次我们需要求的是f[i,i+1]。所以为了少写if,就把初始化变成了这玩意。

  f[i,i]:=0;

end;


for l:=2 to n do

  for i:=1 to n-l+1 do

  begin

   j:=l+i-1;

   for k:=s[i,j-1] to s[i+1,j] do

   begin

    if f[i,j]>f[i,k]+f[k+1,j]+w[j]-w[i-1] then

    begin

     f[i,j]:=f[i,k]+f[k+1,j]+w[j]-w[i-1];

     s[i,j]:=k;//决策时k的值,而且,这里是顺序枚举,可以保证在最优决策数量>1的时候取到最大的k,从而进一步缩小下一次k的枚举范围。

    end;

   end;

  end;


writeln(f[1,n]);

end.



三个石子归并都over了。所谓数据越大越是激发思考者的大脑。三个石子归并一完,不写题解不知道,一写题解才发现原来区间DP还有如此多的解法。所谓经典,以此为称。

DP路上,希望与你们共同进步。

评论
热度(6)
©主题 —— Powered by LOFTER