hdu 5884 二分+k叉哈夫曼树

可以先看一下k叉哈夫曼树的构造方法,大佬说的很明白传送门:https://blog.csdn.net/AngOn823/article/details/52598438

带权路径:是树中所有的叶结点的权值乘上其到根结点的路径长度。

哈夫曼树就是带权路径最小的树。


有n个数(即n个叶子节点),构造k叉(k>=2)哈夫曼树的方法;

构造哈夫曼树,其实就是不停的“合并”的过程。并且每次合并,我们都是取前k个最小的数。想的到,算法的主要复杂就在于如何取前k个最小的数;不停排序或者优先队列、堆都可以做到,但复杂都接nlogn,那么我们可不可以只排序一次就能每次都取出前k个数呢?

答案是能!

我们可以维护两个数组利用类似与“归并排序”的想法,O(n)的构造出k叉哈夫曼树。

第一步:先将n个数从小到大排序一次,放入第一个数组a;取k个数合并成一个新的数放入数组b的末尾。

之后,每次从两个数组里挑选出k个最小的数合并再次放到数组b的末尾。

这里不难发现:b数组是有序的。因为每次放入的都是最小的k个数之和,第二次放的肯定比第一次大。

那么也就是a,b数组都是有序的,所以之前说的从两个数组里取k个最小的数出来也就不难做到,只要维护两个指针,都指向数组的第一个位置,然后每次比较谁小就取谁,被取的那个指针往后移一位。

最后,当a数组为空,b数组只剩一个数时算法结束。(这个数即哈夫曼的根)

在以上的描述中,为了方便我省略了一个细节:

注意到,每次我们都是取k个数,但在最后一次取的时候,可能已经不足k个数可取了,所以不妨在算法的开始,我们就先取掉x个数,使得剩下的n-x个数正好能每次取k个取完。

算法总的来说,就是两个指针从首扫到尾,时间复杂度不超过O(2*n),已经是非常优秀。


关于k叉哈夫曼树在ACM中的最常用的应用无疑就是求合并n个数最小的代价。


HDU 5884
4198: [Noi2015]荷马史诗

然后我们来看这道题HDU 5884

                                                                              Sort

Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)

Problem Description
Recently, Bob has just learnt a naive sorting algorithm: merge sort. Now, Bob receives a task from Alice.
Alice will give Bob N sorted sequences, and the i-th sequence includes ai elements. Bob need to merge all of these sequences. He can write a program, which can merge no more than k sequences in one time. The cost of a merging operation is the sum of the length of these sequences. Unfortunately, Alice allows this program to use no more than T cost. So Bob wants to know the smallest k to make the program complete in time.
 
Input
The first line of input contains an integer t0, the number of test cases. t0 test cases follow.
For each test case, the first line consists two integers N (2≤N≤100000) and T (∑Ni=1ai<T<231).
In the next line there are N integers a1,a2,a3,...,aN(∀i,0≤ai≤1000).
 
Output
For each test cases, output the smallest k.
 
Sample Input
1
5 25
1 2 3 4 5
 
Sample Output
3

题意:n个有序序列的归并排序.每次可以选择不超过k个序列进行合并,合并代价为这些序列的长度和.总的合并代价不能超过T, 问k最小是多少。

首先我们二分查找k值,然后查询每个k,判断是否符合要求

然后就是k叉哈夫曼树的构造了

一共有n个数共需归并n-1次数,一次归并k-1个数

所以当(n-1)%(k-1) != 0时就会出现归并不能最大化个数的情况,这样会影响二分的单调性,我们可以在开始先取(n-1)%(k-1)个数进行求和,或者在这组数前面加上  n-1-((n-1%(k-1))个0,这里用第二种。

接下来的数我们已经知道了肯定是(n-1)/(k-1)+1块了,所以可以直接想到放进优先队列,然后每次都拿前k个数,新的数继续丢进去,但是这样会T

那么?我们这样处理,先把0放入q中,将其余的数丢进队列q中,再开一个队列p,每次新的数丢进p中。每次在q和p中一共拿k个小的数来组成一个新的数,新的数丢到p队尾去。直到p,q都为空的时候结束循环,ans计算总的代价和接下来证明为什么不丢进优先队列,反而队列就可以了(也就是为什么p都是排好序的?),每次新丢进来数都是由前k个小的数取和得来的,所以每次丢进队尾的数都比前面丢进去的数要大,随手画一下就可以了,然后拿完q再去拿p。

k值越大其重复的节点数越少,代价和也就越小,所以k合并代价<=T时我们可以把k赋值给右边界,找更小的k,当k合并代价>T时,我们就找更大的k。

下面是AC代码

#include<iostream>
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 100005;
//#define INT_MAX 0x7fffffff
int n,m; 
int a[N];	
queue <ll> q,p;
int Is_Fit(int k){//3 
    while(!q.empty()) q.pop();
	while(!p.empty()) p.pop();
    ll  sum=0,ans=0;
    if(((n-1)%(k-1))){
    	for(int i = 1 ;i <= k-1-((n-1)%(k-1)) ; i++)
    	    q.push(0);
	}
    for(int i = 1; i <= n; i++){
    	q.push(a[i]);
	}
   while(1){
    	//int b1=INT_MAX,b2=INT_MAX;
    	int b1,b2;
    	sum = 0;
    	for(int i=1;i<=k;i++){
    		if(q.empty()&&p.empty())
		        break;
		     if(q.empty() ){
		     	sum+=p.front();p.pop();
		     	continue;
			 } 
			 if(p.empty()){
			 	sum+=q.front();q.pop();
			 	continue;
			 }
			 b1=q.front();
			 b2=p.front();
			 if(b1<b2){
			 	sum+=b1;q.pop();
			 }
			 else {
			 	sum+=b2;p.pop();
			 }
		} */
    	//不能这样写,如果p为空则b2的取值会成问题,也不能赋初值给b2=1005;虽然其取值范围小于1000,但是中间的和即节点的值不一定小于1005	....那么问题来了b1,b2赋了最大值为什么还是错误???? 
    	/*if(q.empty()&&p.empty())
		    break;
		if(!q.empty()){
				b1 =  q.front();
			}
		if(!p.empty()){
			    b2 = p.front();
			    //p.pop() ;
			}
			//cout<<b1<<" "<<b2<<"######       ";
		    if(b1>b2) {
			    sum+=b2;
			    p.pop();
			  //  cout<<"@@"<<sum<<"@@"; 
			}
	 	    else{
	 	    	
	 	    	sum+=b1;
				 q.pop();
			 }
			 //cout<<"*****    "<<sum<<"  ";
		}
		*/ 
		ans += sum;
	    if(q.empty()&&p.empty())
		break; 
		p.push(sum); 
    }
	if(ans>m)  return 0;
	return 1;
}
void Bi_Search(){
	int left=2,right=n;
	while(left<right){
	    int k =left + (right -left)/2 ;//(left+right)/2;
	    if(Is_Fit(k)){
		    right = k; 
	    }
	    else{
		    left = k+1;
	    }
	} 
	printf("%d\n",right);
}

int main(){
	int t;
	cin>>t;
	while(t--){
	    cin>>n>>m;
		for(int i=1;i<=n;i++)
		cin>>a[i];	
		sort(a+1,a+n+1);
		Bi_Search();
	}
	return 0;
}
发布了15 篇原创文章 · 获赞 1 · 访问量 4147

猜你喜欢

转载自blog.csdn.net/jingli456/article/details/89361702