8.6 竞赛题目选讲
量力而行
8-10 抄书 (UVA 714)
把一个包含m个正整数的划分成k个(1<=k<=m<=500)非空的连续子序列,使得每个正整数恰好属于一个序列。设第i个序列的各数之和为S(i),你的任务是让max{S(i)}最小,输出依次个数字最小的情况。
分析:我们现在将这m个整数分割成的区间的区间和的最大值设为x,根据这个x所能够最多分割出的区间个数我们记作P(x),于是这个问题转化为求解的P(x)=k的最小解。
然后我们知道P(x)这个函数的值是随着x的增大而不严格递减的,也就是说我们可以通过求下界的二分查找法去得到我们的答案。
大致的思路如上,但是具体的细节还有很多需要补充的地方,比如在打印答案时,需要进行一个反向的贪心(注意进行贪心策略之前都需要数据满足贪心的条件)来确定分割的情况,细节和代码如下:
#include<iostream>
using namespace std;
typedef long long ll;
ll n,k,a[501],l=0,r=0;//l和r分别为二分的两个结点
//num(x)表示以x作为每个区间和的可允许的最大值,所得到的区间个数
int num(ll x){
ll s=0,num_=1;//区间个数默认为1
//当确定新加的一个数会超过可允许的最大值时,就增加一个区间,此时新增加的区间默认已经放入新加的数
for (int i=0;i<n;i++) if (s+a[i]>x) {
num_++; s=a[i];} else s+=a[i];
return num_;
}//如果有多解,要求是前面的区间和尽可能的小,所以应该选取的策略是从右往左倒着贪心
//这样可以保证后面的区间和尽可能地接近我们得到的每个区间和可允许的最大值
//当前面的数字出现不够按照贪心的策略分配的趋势,就直接将剩余的数字一一分配,保证第一个区间只有a[i]即可
void print_ans(ll x){
ll s=0,num_=k; int is_sep[501]={
0};//is_sep[i]表示第i个数字后面是否有分隔符
for (int i=n-1;i>=0;i--){
//我开始没有注意到这个问题中贪心策略存在前提
//就是说你每次进行贪心策略前剩余的数字个数需要满足剩余的区间里至少可以每个区间分配一个数字
//num_表示剩余的区间个数,i+1表示剩余的数字个数,当剩余的区间个数大于剩余的数字个数时
//就需要将当前的区间结束(不再添加新数,此时剩余区间个数和剩余数字个数相等),然后一一分配
if (num_>i+1) {
is_sep[i]=1; num_--;}//一一分配
else if (a[i]+s>x) {
is_sep[i]=1; num_--; s=a[i];}//和前面一致的反向贪心
else s+=a[i];
}
for (int i=0;i<n;i++) {
cout<<a[i]<<" "; if (is_sep[i]) cout<<"/ ";}
}//我这里原本是考虑将l直接定义为0,看了别人的题解意思是如果直接定义为0做二分的话会有一点点超时
int main(){
cin>>n>>k; for (int i=0;i<n;i++) {
cin>>a[i]; l=max(l,a[i]); r+=a[i];}
//注意这里的二分法,是在有多个解时,求下界的二分查找法,所以得到应该是最小的区间和
while (l<r){
ll m=(l+r)/2; (num(m)>k)?l=m+1:r=m;
}print_ans(l); return 0;
}
8-11 全部相加 (UVA 10954)
给你n个数的一个集合S,每次从集合S中删除两个数,然后将这两个数的和放回集合,直到剩下最后一个数。每次操作的开销等于被删除的两个数字之和,求最小的总开销。
分析:这个就是求Huffman编码的过程,有若干个数每个数都有一个值(Huffman编码里面是频率),然后将任意两个树相加合并成一颗新的树,越下层的结点被加的次数越多。于是我们模仿Huffman编码的建立进行编写就可以了,代码如下:
#include<iostream>
#include<queue>
using namespace std;//这里不建立二叉树,而是通过优先队列每次取当前最小的两个数
int