【竞赛题目对比】& 合并吧!石子合并 与 合并果子 &

来源:互联网 发布:eia原油库存数据 编辑:程序博客网 时间:2024/06/05 10:39

石子合并 与 合并果子

  • 石子合并 与 合并果子
    • 前言
    • 题目简介
    • 合并果子-NOIP2004提高组
      • 题目描述
      • 分析
      • 最优生成树
      • 最优生成树-合并果子
      • 最优生成树-构造
      • 最优生成树-正确性证明
      • 实现-堆-优先队列
    • 石子合并-削弱版
      • 题目描述
      • 分析
      • 动规-区间DP
        • 区域DP-问题结构
        • 区域DP-状态
        • 区域DP-状态转移方程式
      • 深度解析-代码
    • 石子合并-原难度NOI1995
      • 题目描述
      • 异同处
      • 分析-代码
    • 感想-吐槽-思考
    • END


前言

嗯嗯你们没有看错:这是两个不同的题目,来自两场不同的比赛,用了两种不同的算法,会出两个不同的答案。我知道电脑【还是手机?】前的你好奇心已经爆棚到不行了,但是还请等等,且听我慢慢道来【被打】

题目简介

<合并果子>有N堆果子排成一行,现要将果子合并成一堆,每次只能合并任意的2堆果子,合并的花费定义为新合成的一堆果子的数量。求这N堆果子合并成一堆的最小总花费。
<石子合并>有N堆石子排成一行,现要将石子合并成一堆,每次只能合并相邻的2堆石子,合并的花费定义为新合成的一堆石子的数量。求这N堆石子合并成一堆的最小总花费。
不知道你们是否看出了区别【除了石子和果子!】。
区别就在于:果子是选择任意的两堆进行合并,而石子选择的是相邻两堆进行合并,这就是导致它们算法不同的根本原因【透露一下,这两个一个是DP,一个是贪心策略,但至于哪个是哪个……】。
【另外,石子合并还有进阶版,把石子摆放弄成了环状,这个我也会讲解】


合并果子-[NOIP2004提高组]

@题目描述@

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。 多多决定把所有的果子合成一堆。每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。 假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将 1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为 12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。

【输入】
第1行:一个整数n(1≤n≤10000),表示果子的种类数。第2行:包含n个整数,用空格分隔,第i个整数ai(1≤ai≤20000)是第i种果子的数目。

【输出】
一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于2^31。

【样例输入】
3
1 2 9
【样例输出】
15

【数据规模】: 对于30%的数据,保证有n≤1000;对于50%的数据,保证有n≤5000;对于全部的数据,保证有n≤10000。

@分析@

此题要求的是最小值【也就是最优化问题】,于是优先考虑到动态规划。但是仔细一读题,我们发现这道题并不满足无后效性,所以动态规划这条道是行不通的。对于搜索来说,数据显得有点大,O(2^n)的算法显然爆。那么,贪心如何?
稍有常识的人即可看出,这道题与贪心的典例->【最优生成树】是差不多的。
啥?你不会连【最优生成树】都不知道吧?

@最优生成树@

【定义】

路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长L-1
结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
最优生成树:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
【来自百度百科】

二叉树例子
这棵二叉树每一个非叶结点上的权被定义为左右儿子的权和。通过观察,我们可以发现其实这棵二叉树的带权路径长度也可以定义为非叶结点的权和【可证】。

@最优生成树->合并果子@

问题又来了:刚才我们讨论了这么一大堆有什么用?
如果我们将原始的果子堆当作生成树的叶结点,将合并的果子堆当作生成树的非叶结点,我们便可以将问题转化为【求生成树的非叶结点的最小权和】。这不就是生成树的带权路径长度的定义嘛!这个问题不正是求最优生成树嘛!于是问题进一步地转变成->【已知生成树的子节点,求最优生成树】

@最优生成树->构造@

(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
【依然是百度百科~链接见上~】

如果应用于合并果子,步骤应该是这样的:
(1)找出最小的两堆果子
(2)将两堆果子数之和加到代价里
(3)移除两堆果子,放入合并的一堆
(4)重复1~3步,直到果子堆数=1

@最优生成树->正确性证明@

【注意!以下内容可能过于枯燥且无味!请小心食用!】
【以下证明来自《算法导论》:】
我们定义函数frep(x)来表示结点x的权,d(x,T)表示结点x在生成树T中的深度。
引理1:若x,y是权最小的两个叶结点,那么存在一个最优生成树,使得x,y是兄弟结点,且它们深度最深。
哈夫曼树证明-例
【证明】:T是任意一种最优生成树【如图】,且a,b为其深度最深的兄弟结点。不失一般性,我们不妨假设frep(y)>=frep(x)frep(b)>=frep(a)。因为我们已经假定x,y权最小,所以必有frep(b)>=frep(a)>=frep(y)>=frep(x)
现在我们通过交换结点a与结点x,生成新树T。那么T带权路径长度T带权路径长度的差为(frep(a)d(a,T)+frep(x)d(x,T))(frep(a)d(a,T)+frep(x)d(x,T))
(frep(a)d(a,T)+frep(x)d(x,T))(frep(a)d(x,T)+frep(x)d(a,T))
进一步简化为(frep(a)frep(x))(d(a,T)d(x,T))>=0
所以T带权路径长度>=T带权路径长度,也就是说T只会更好不会更差。
同理,交换结点b与结点y所形成的新树只会更好不会更差。
原命题得证。
引理2:设TT去掉权值最小叶结点x,y,将它们父亲的权设为x的权与y的权之和的这样一棵树。假设T是最优生成树,则T也是最优生成树。
【证明】:先用T去表示原树T,可以得到T带权路径长度T带权路径长度的差是frep(z)d(z,T)(frep(x)+frep(y))d(y,T)。因为d(z,T)=d(y,T)1frep(z)=frep(x)+frep(y),代入原式可得T带权路径长度=T带权路径长度-(frep(x)+frep(y))
用反证法。假设最优生成树为F而不是T,不失一般性的,由引理1可知权值最小叶结点x,y是兄弟结点。令FF去掉x,y,将它们父亲的权设为x的权与y的权之和的这样一棵树。于是:
F带权路径长度=F带权路径长度-(frep(x)+frep(y))<T带权路径长度-(frep(x)+frep(y))=T带权路径长度
与命题中的T是最优生成树矛盾。
原命题得证。
于是最优生成树的构造一定成立。

@实现->堆->优先队列@

于是我爽爽地按照思路去实现:

#include<cstdio>const int MN=10000;int a[2*MN+5]={2*MN+1};bool b[2*MN+5];int min(int x){    int min=0;    for(int i=1;i<=x;i++)        if(b[i]==0&&a[i]<a[min]&&a[i]!=0) min=i;    b[min]=true;    return a[min];}int main() {     int n,x,ans=0;     scanf("%d",&n);     for(int i=1;i<=n;i++)        scanf("%d",&a[i]);    for(int i=1;i<n;i++)     {         int p=min(n+i);        int q=min(n+i);        ans+=(p+q);         a[n+i]=p+q;     }     printf("%d\n",ans); } 

O(n^2)算法,结果你猜怎样?
超时。
嗯,所以还得再改。
其实我们寻找最小值的时候,可以使用一种名叫【小根堆】的数据结构,只需要O(lg n)的时间复杂度即可得出最小值。这么一来,总体的时间复杂度就为O(n*lg n)了。超时?不存在的!
堆,可以用c++中的STL的优先队列,也可以手动写
AC代码:

#include<cstdio> #include<queue> using namespace std; priority_queue<int>a; int main() {     int n,x,ans=0;     scanf("%d",&n);     for(int i=1;i<=n;i++)     {         scanf("%d",&x);         a.push(-x); //大根堆取负,就成了小根堆    }     for(int i=1;i<n;i++)     {         int p=a.top();         a.pop();         int q=a.top();         a.pop();         ans-=(p+q);         a.push(p+q);     }     printf("%d\n",ans); } //STL
#include<cstdio>#include<cstring>int a[20005],len;void swap(int &a,int &b){    int t=a;a=b;b=t;}void insert(int x){    a[++len]=x;    int now=len;    while(a[now/2]>a[now]&&now!=1)    {        swap(a[now/2],a[now]);        now/=2;    }}int top(){    int k=a[1],now=1;    a[1]=a[len--];    a[len+1]=2139062143;    while(a[now*2]<a[now]||a[now*2+1]<a[now])    {        if(a[now*2+1]<a[now*2])        {            swap(a[now*2+1],a[now]);            now=now*2+1;        }        else        {            swap(a[now*2],a[now]);            now=now*2;        }    }    return k;}int main(){    int n,ans=0;    memset(a,127,sizeof(a));    scanf("%d",&n);    for(int i=1;i<=n;i++)    {        int x;        scanf("%d",&x);        insert(x);    }    for(int i=1;i<n;i++)    {        int p=top();        int q=top();        ans+=(p+q);        insert(p+q);    }    printf("%d\n",ans);}//手写

石子合并-削弱版

@题目描述@

设有n堆石子排成一排,其编号为1,2,3,…,n。每堆石子有一定的数量,例如:13 7 8 16 21 4 18 。
现要将n堆石子归并为一堆。归并的过程为每次只能将相邻的两堆石子堆成一堆,这样经过n-1次归并之后最后成为一堆。对于上面的7堆石子,可以有多种方法归并成一堆。其中的2种方法如下图:
方法(1):
石子合并-弱-题图1
方法(2):
这里写图片描述
归并的代价是这样定义的:将两堆石子归并为一堆时,两堆石子数量的和称为归并2堆石子的代价。如上图中,将13和7归并为一堆的代价为20。归并的总代价指的是将沙子全部归并为一堆沙子的代价的和。如上面的2种归并方法中,第1种的总代价为 20+24+25+44+69+87 = 267;第2种的总代价为 15+37+22+28+59+87 = 248。
由此可见,不同归并过程得到的总的归并代价是不一样的。 当n堆石子的数量给出后,找出一种合理的归并方法,使总的归并代价为最小。

输入
第1行:1个整数n(1<=n<=100),表示石子的数量
第2行:n个用空格分开的整数,每个整数均小于10000,表示各堆石子的数量。

输出
第1行:1个整数,表示最小的归并代价

样例输入
3
13 7 8
样例输出
43

@分析@

最优化问题,且满足最优子结构性质【如果石堆A是由B与C合并而来的,那么A的最小总代价意味着B与C的最小总代价】,让人不得不联想到DP。事实上,这一题就是经典的区域DP模型。啥?就算你不知道【最优生成树】,总得知道区域DP吧。

@动规->区间DP@

动态规划一般可分为线性动规区域动规树形动规背包动规四类。

如同它们的名字所描述的,它们就是解决专门某一类型的DP。背包动规对应着背包问题,典例有【0-1背包】,【完全背包】,【多重背包】,【分组背包】等;树形动规对应着有关【树】的动规问题,典例有【贪吃的九头龙】,【聚会的欢乐】,【数字三角形】等;线性动规对应着一般性动规问题,典例有【拦截导弹】,【合唱队形】,【挖地雷】,【渡轮问题】等。
而区域动规,对应着的就是有关【区间】的动规问题。典例有【乘积最大】,【书的复制】,【加分二叉树<-对这不是树形DP】。我们目前看到的这道题也属于这一类。
区域动规有什么相似点吗?有的。我们从问题结构->状态->状态转移方程式一步步来说:

区域DP->问题结构

身为动规,自然要符合动规这个大家庭的规矩:最优子结构和无后效性。最优子结构无需说,因为区间是由两个小区间推导而来;无后效性主要体现在,区间的最优值只能作用于相邻的区间。敏锐的抓住这两点,就可以快速判断出题目的类型了

区域DP->状态

区域DP和区间有关系,状态的定义当然与区间脱不了干系。我通常用两种方法定义区域DP的状态——一是用f[i][j]表示区间[i,j]的最优值,一是用f[i][len]表示起点为i,长度为len的区间的最优值。个人喜欢后一种,因为后一种可以使用滚动数组节省空间。状态的初值就是f[i][1]

区域DP->状态转移方程式

我们分析问题结构时,有提到大区间的最优值是由小区间【而且是相邻的】推导而来的。那么一般地,状态转移方程式有如下形式:

f[i][j]=max(f[i+1][j],f[i][j1])+g[i][j]

深度解析-代码

在这道题中,我们定义状态f[i][len]为【以i为起点,长度为len的石堆合并的最小代价】,辅助数组g[i][j]为第i堆石子到第j堆石子的石子和,目标状态为f[1][n],初始化f[i][1]为0。然后最重要的状态转移方程式就是——

f[i][len]=max(f[i][len],f[i][ki+1]+f[k+1][jk]+g[i][j])

其中,j=i+len-1,k是要合并的两堆石子的断开点。
完整的,AC的代码如下:

#include<cstdio>const int INF=1<<30;int f[105][105];int g[105][105];int min(int a,int b){    if(a<b) return a;    else return b;}int main(){    int n;    scanf("%d",&n);    for(int i=1;i<=n;i++)        scanf("%d",&g[i][i]);    for(int i=1;i<=n;i++)        for(int j=i+1;j<=n;j++)            g[i][j]=g[i][j-1]+g[j][j];    for(int len=2;len<=n;len++)        for(int i=1;i<=n-len+1;i++)            f[i][len]=INF;    for(int len=2;len<=n;len++)        for(int i=1;i<=n-len+1;i++)        {            int j=i+len-1;            for(int k=i;k<j;k++)                f[i][len]=min(f[i][len],f[i][k-i+1]+f[k+1][j-k]+g[i][j]);        }    printf("%d\n",f[1][n]);}}

石子合并-原难度[NOI1995]

@题目描述@

题目描述
在一圆形操场四周摆放N堆石子 , 现要将石子有次序地合并成一堆.规定每次只能选相临的两堆合并成一堆,并将新的一堆的石子数,记为该次合并的得分。
石子合并-强-题图
编一程序,由文件读入堆数N及每堆石子数,
(1)选择一种合并石子的方案,使得做N-1次合并,得分的总和最少
(2) 选择一种合并石子的方案,使得做N-1次合并,得分的总和最大

输入
第1行:1个整数n(1<=n<=100),表示石子的数量
第2行:n个用空格分开的整数,每个整数均小于10000,表示各堆石子的数量。

输出
第1行:1个整数,表示最小的归并代价
第2行:1个整数,表示最大的归并代价

样例输入
3
13 7 8
样例输出
43
49

@异同处@

这既然是从上面一题的铺垫而来,那就肯定与上一题或一样,或变动。

@同@

很明显,状态的定义是完完全全一样的,初始化的东西也是一样的,状态转移方程式也是一样的。感觉简直就是一个题啊喂!?
不同点呢?

@异@

这道题目和上一道题目到底差在哪儿?
从题目描述来讲,这一题的石堆摆成了环形,因此我们可以合并第n堆与第1堆。我们不能再用g[i][j]来表示第i堆石子到第j堆石子的石子和了,因为会出现两种情况:顺时针合并与逆时针和合并。
目标状态也变得有些不一样了。因为环形,我们可以以任意一堆石子作为起始,而不是固定为1。
甚至连输出都不一样了,还要输出最大合并代价与最小合并代价
这使得这道题解法变得不太一样

分析->代码

我们一步步地改变【异】点:
1).定义g[i][j]为【以i为左起点,往右数j堆石子之和】固定i为左起点,固定合并方向为顺时针。
2).在状态f[1...n][n]中寻找最优解。
3).最大合并代价?把最小合并代价中的min()->max()就可以了。
代码:

#include<cstdio>#include<cstring>#include<algorithm>using namespace std;const int INF=1<<30;int f[105][105];int g[105][105];int n;int F(int x){    int k=x%n;    if(k==0) k=n;    return k;}void solve_Min(){    for(int len=2;len<=n;len++)        for(int i=1;i<=n;i++)        {            int j=i+len-1;            for(int k=i;k<j;k++)                f[i][len]=min(f[i][k-i+1]+f[F(k+1)][j-k]+g[i][len],f[i][len]);        }    int Min=INF;    for(int i=1;i<=n;i++)        Min=min(Min,f[i][n]);    printf("%d\n",Min);}void solve_Max(){    for(int len=2;len<=n;len++)        for(int i=1;i<=n;i++)        {            int j=i+len-1;            for(int k=i;k<j;k++)                f[i][len]=max(f[i][k-i+1]+f[F(k+1)][j-k]+g[i][len],f[i][len]);        }    int Max=-INF;    for(int i=1;i<=n;i++)        Max=max(Max,f[i][n]);    printf("%d\n",Max);}int main(){    scanf("%d",&n);    for(int i=1;i<=n;i++)        scanf("%d",&g[i][1]);    for(int len=2;len<=n;len++)        for(int i=1;i<=n;i++)            g[i][len]=g[i][len-1]+g[F(i+len-1)][1];    for(int len=2;len<=n;len++)        for(int i=1;i<=n;i++)            f[i][len]=INF;    solve_Min();    for(int len=2;len<=n;len++)        for(int i=1;i<=n;i++)            f[i][len]=-INF;    solve_Max();}

@感想-吐槽-思考@

这两道题都是NOI(P)比赛的原题,所以难度对于如今的我还是算【hard】的。特别是最后的那道题,若是让我不经【削弱版】的洗礼,还真成一个问题。
不得不说出题人到底是什么心态,找了两道如此相近的题来考【喂喂明明是你自己要把它们合着讲】,却又是如此大相径庭的模型->最优生成树&&区域DP。
虽然只是简简单单的两道题目,但是从这两道题目中我得到的又不只两道题目的精华,反而是很多很多。可能题目就是这样,永远不会相同,但是永远相通。从一道题上升,到两道题,到三道题,到一个类,到两个类,再到题目本身,这有很多路要走。
但是不管路有多远,我都会走下去。

END

就是这样,新的一天里,也请多多关照哦(ノω<。)ノ))☆.。~

原创粉丝点击