【题目链接】
ybt 1369:合并果子(fruit)
ybt 1836:【04NOIP提高组】合并果子
洛谷 P1090 [NOIP2004 提高组] 合并果子
注:ybt 1369 中n的最大值为30000,而ybt 1836与洛谷 P1090中n的最大值为10000
本题代码默认n的最大值为30000,可以通过以上各问题。
【题目考点】
1. 贪心
2. 堆/优先队列
3. 哈夫曼树
【解题思路】
解法1:贪心
1. 贪心选择性质的证明:
贪心选择:选择其中果子数量最小的两堆进行合并
设初始情况下,各堆果子的数量为
a
1
,
a
2
,
.
.
.
,
a
n
a_1, a_2, ..., a_n
a1,a2,...,an,其中最少的两堆为
a
g
a_g
ag与
a
h
a_h
ah
最优解指的是将所有果子合并成一堆的消耗体力最小的合并操作序列。
将两堆数量为a与b的果子合并为一堆,记为<a, b>。
- 证明1:存在最优解包含第一次的贪心选择
要证明最优解包含第一次的贪心选择,即证明存在最解中包含合并操作
假设:所有的最优解中不包含合并操作< a g a_g ag, a h a_h ah>。
假设该最优解中包含合并操作< a g a_g ag, a x a_x ax>,< a h a_h ah, a y a_y ay>
显然有: a g , a h ≤ a x , a y a_{g}, a_{h} \le a_{x}, a_{y} ag,ah≤ax,ay
情况1:如果 a y a_y ay中不包含 a g a_g ag,且 a x a_x ax中不包含 a h a_h ah。也就是说< a g a_g ag, a x a_x ax>,< a h a_h ah, a y a_y ay>合并成两堆后,又经过了多次合并后形成了一个堆。
设合并操作< a g a_g ag, a x a_x ax>,< a h a_h ah, a y a_y ay>完成合并后的这堆果子又参与了 d 1 d_1 d1次合并。合并操作< a h a_h ah, a y a_y ay>完成合并后的这堆果子又参与了 d 2 d_2 d2次合并。
不失一般性,可以假设 d 1 ≤ d 2 d_1 \le d_2 d1≤d2。
那么这两次合并操作造成的体力消耗为 a g + a x + a h + a y a_g+a_x+a_h+a_y ag+ax+ah+ay,这两次操作合并后这两堆果子在更大的一堆果子中参与合并造成的体力消耗为 d 1 ( a g + a x ) + d 2 ( a h + a y ) d_1(a_g+a_x)+d_2(a_h+a_y) d1(ag+ax)+d2(ah+ay),总消耗为 ( d 1 + 1 ) ( a g + a x ) + ( d 2 + 1 ) ( a h + a y ) (d_1+1)(a_g+a_x)+(d_2+1)(a_h+a_y) (d1+1)(ag+ax)+(d2+1)(ah+ay)
那么如果将< a g a_g ag, a x a_x ax>,< a h a_h ah, a y a_y ay>。这两组操作替换为:< a g a_g ag, a h a_h ah>, < a x a_x ax, a y a_y ay>。
< a g a_g ag, a h a_h ah>合并成的堆代替原来< a g a_g ag, a x a_x ax>合并成的堆参加后面的操作,参与 d 1 d_1 d1次合并。
< a x a_x ax, a y a_y ay>合并成的堆代替原来< a h a_h ah, a y a_y ay>合并成的堆参加后面的操作,参与 d 2 d_2 d2次合并。
这两次合并以及这两次操作合并后这两堆果子参与合并造成的体力消耗为 ( d 1 + 1 ) ( a g + a h ) + ( d 2 + 1 ) ( a x + a y ) (d_1+1)(a_g+a_h)+(d_2+1)(a_x+a_y) (d1+1)(ag+ah)+(d2+1)(ax+ay)
这两个合并操作造成的两堆果子的变化是造成总体力消耗变化的唯一因素。
因此替换合并操作前这四堆果子造成的体力消耗减去替换合并操作后这四堆果子造成的体力消耗,为:
( d 1 + 1 ) ( a g + a x ) + ( d 2 + 1 ) ( a h + a y ) − ( d 1 + 1 ) ( a g + a h ) − ( d 2 + 1 ) ( a x + a y ) (d_1+1)(a_g+a_x)+(d_2+1)(a_h+a_y) - (d_1+1)(a_g+a_h)-(d_2+1)(a_x+a_y) (d1+1)(ag+ax)+(d2+1)(ah+ay)−(d1+1)(ag+ah)−(d2+1)(ax+ay)
= d 1 ( a x − a h ) + d 2 ( a h − a x ) =d_1(a_x-a_h)+d_2(a_h-a_x) =d1(ax−ah)+d2(ah−ax)
= ( d 1 − d 2 ) ( a x − a h ) =(d_1-d_2)(a_x-a_h) =(d1−d2)(ax−ah)
已知 d 1 ≤ d 2 d_1\le d_2 d1≤d2且 a x ≤ a h a_x\le a_h ax≤ah
所以 ( d 1 − d 2 ) ( a x − a h ) ≥ 0 (d_1-d_2)(a_x-a_h)\ge0 (d1−d2)(ax−ah)≥0
因此替换合并方案后,体力消耗减少或不变。这是个包含贪心选择 < a g a_g ag, a h a_h ah>的最优解,与假设相悖。原命题得证。
2. 如果 a y a_y ay中包含 a g a_g ag,也就是说< a g a_g ag, a x a_x ax>合并后,再与一些其他堆经过 d d d次合并得到了 a y a_y ay,而后进行< a h a_h ah, a y a_y ay>。
< a g a_g ag, a x a_x ax>消耗体力 a g + a x a_g+a_x ag+ax,得到一个堆。这个堆参与合并直到变成 a y a_y ay,消耗体力 d ( a g + a x ) d(a_g+a_x) d(ag+ax),共消耗体力 ( d + 1 ) ( a g + a x ) (d+1)(a_g+a_x) (d+1)(ag+ax)。
将以上过程中的 a x a_x ax与 a h a_h ah做交换,其它操作不变。
即先将< a g a_g ag, a h a_h ah>合并,而后这个堆经过 d d d次与之前相同的合并后变为 a y ′ a_y' ay′。而后进行< a x a_x ax, a y ′ a_y' ay′>。合并成与先前一样大的堆(因此< a h a_h ah, a y a_y ay>与< a x a_x ax, a y a_y ay’>消耗的体力相同),而后还是可以进行相同的合并操作。
合并< a g a_g ag, a h a_h ah>消耗体力 a g + a h a_g+a_h ag+ah,后续合并直到变为 a y ′ a_y' ay′消耗体力 d ( a g + a h ) d(a_g+a_h) d(ag+ah),共消耗体力 ( d + 1 ) ( a g + a h ) (d+1)(a_g+a_h) (d+1)(ag+ah)
由于 a h ≤ a x a_h\le a_x ah≤ax,所以有 ( d + 1 ) ( a g + a h ) ≤ ( d + 1 ) ( a g + a x ) (d+1)(a_g+a_h) \le (d+1)(a_g+a_x) (d+1)(ag+ah)≤(d+1)(ag+ax)
因此将 a x a_x ax与 a h a_h ah做交换后,消耗的体力减少或不变。这是个包含贪心选择 < a g a_g ag, a h a_h ah>的最优解,与假设相悖。原命题得证。
3. 如果 a x a_x ax中包含 a h a_h ah,也就是说< a h a_h ah, a y a_y ay>合并后,再与一些其他堆经过 d d d次合并得到了 a x a_x ax,而后进行< a g a_g ag, a x a_x ax>。
该情况与第2点情况相同,证明方法略。
- 证明2:假设最优解包含前k次的贪心选择,证明最优解包含第k+1次的贪心选择
经过前k次的贪心选择后,还剩下n-k个堆。该情况与证明1面对的情况相同,可以使用相同的方法证明最优解一定包含此时的贪心选择。
为了能在
O
(
l
o
g
n
)
O(logn)
O(logn)复杂度内取到所有果堆中数量最小的一堆,也就是取当前多个数字中的最小值,需要用到堆(heap)这一数据结构,C++ STL中提供了以堆为原理的优先队列(priority_queue),可以使用该容器求数字中的最小值。
将优先队列设为小顶堆,将n个数字加入到优先队列之中,每次出队两个数字,出队的两个数字就是最小的两个数字,将这两个数字加和,即为合并两堆果子。设sum
记录消耗的体力,sum
增加这两个数的加和。而后将这个加和加入到优先队列中。
最后sum
即为消耗的最少体力。
解法2:构建哈夫曼树
果子合并的过程可以画成一棵二叉树。
每个结点表示一堆果子。
如果a, b两堆合为一堆,则可以画一个根结点g表示a,b合并后的果堆,这个结点的左孩子是a,右孩子是b。
整棵树的根结点就是把所有堆合并后的堆。
下图中,冒号后面的数字表示果子数量。其中堆c经过2次合并,d经过2次合并,a与b经过1次合并。
看得出一个堆经过合并的次数,就是从根结点到这个堆的路径长度。
一个堆对体力的消耗为这个堆中果子数量乘以这个堆经过合并的次数。
若以堆中果子数量为权值,那么一个堆消耗的体力正是该结点的带权路径长度。
所有堆合并在一起消耗的体力,就是整棵树的带权路径长度。
已知哈夫曼树的带权路径长度是最小的。
我们只需要让每个堆为一个结点,结点的权值就是堆中果子数量。以这些结点构建出哈夫曼树。
写法1:求出这棵哈夫曼树的带权路径长度,即为将所有果子合在一起消耗的最少体力。
写法2:哈夫曼树中每个分支节点的权值,就是合并成这堆果子消耗的体力,因此总消耗的体力就是所有分支节点权值加和。
【题解代码】
解法1:贪心
#include <bits/stdc++.h>
using namespace std;
int main()
{
priority_queue<int, vector<int>, greater<int> > pq;//小顶堆
int n, a, sum = 0;//sum:体力加和
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> a;
pq.push(a);
}
while(pq.size() > 1)
{
int a = pq.top();
pq.pop();
int b = pq.top();
pq.pop();
sum += a+b;//体力增加这次合并后的果堆中果子的数量
pq.push(a+b);
}
cout << sum;
return 0;
}
解法2:构建哈夫曼树
- 写法1:求树的带权路径长度
#include<bits/stdc++.h>
using namespace std;
#define N 30005
struct Node
{
int left, right, w;
};
Node node[N];
int p, sum;//sum:树的带权路径长度
struct Cmp
{
bool operator () (int &a, int &b)
{
return node[b].w < node[a].w;
}
};
priority_queue<int, vector<int>, Cmp> pq;
int createTree()
{
while(pq.size() > 1)
{
int np = ++p;
node[np].left = pq.top();
pq.pop();
node[np].right = pq.top();
pq.pop();
node[np].w = node[node[np].left].w + node[node[np].right].w;
pq.push(np);
}
return pq.top();
}
void dfs(int r, int d)//d:深度 深搜求树的带权路径长度
{
if(node[r].left == 0 && node[r].right == 0)
{
sum += d*node[r].w;//sum增加结点r的带权路径长度
return;
}
dfs(node[r].left, d+1);
dfs(node[r].right, d+1);
}
int main()
{
int n, np, root;
cin >> n;
for(int i = 1; i <= n; ++i)
{
np = p++;
cin >> node[np].w;
pq.push(np);
}
root = createTree();
dfs(root, 0);
cout << sum;
return 0;
}
- 写法2:求分支结点的权值加和
#include<bits/stdc++.h>
using namespace std;
#define N 30005
struct Node
{
int left, right, w;
};
Node node[N];
int p;
struct Cmp
{
bool operator () (int &a, int &b)//权值小的结点优先
{
return node[b].w < node[a].w;
}
};
priority_queue<int, vector<int>, Cmp> pq;
int createTree()
{
while(pq.size() > 1)
{
int np = ++p;
node[np].left = pq.top();
pq.pop();
node[np].right = pq.top();
pq.pop();
node[np].w = node[node[np].left].w + node[node[np].right].w;
pq.push(np);
}
return pq.top();
}
int sumW(int r)
{//以r为根结点的树中分支结点的权值加和
if(node[r].left == 0 && node[r].right == 0)
return 0;
return sumW(node[r].left) + sumW(node[r].right) + node[r].w;
}
int main()
{
int n, np, root;
cin >> n;
for(int i = 1; i <= n; ++i)
{
np = p++;
cin >> node[np].w;
pq.push(np);
}
root = createTree();
cout << sumW(root);
return 0;
}