HDOJ-4006/(大连网赛1006)- The kth great number 剖析

本文不想废话,直接上多种做法。

题意:固定的k,动态加点,动态询问第k大数。

一、树状数组+二分

这里有两种做法,一种是二分sum(i),另一种是利用二进制二分逼近k。

树状数组常用来处理区间点的统计情况,这里n没有规定大小(理论上是int32),但是操作次数n是小于1000,000的,所以可以先进行离散化来储存1000,000个点值(我不知道这是不是所谓的离散化,因为点本身是整数,但是,这至少是一种散列、一种映射,异曲同工的)。

具体操作为,先读入样例中出现的所有点,然后排序,insert( lower_bound(list, list+n, a[i])-list+1 ) 。a[]为原始数列,list为排序后数列,+1为了避免下标为0。至此便完成了离散化。

返回第k大数:

① 二分sum(i),利用二分查找的思想,目标为 tot-sum(ans) < k <= tot-sum(ans-1)。sum()为树状数组的求和操作。要注意的是,树状数组中c[x]记录的是x的权值,也就是有c[x]个数同为x,则在查找第k大数的时候不一定是正好的,不能按原始的二分查找的模式,这里WA了好多次......

② 二进制二分逼近,本来是受以下二者启发,
http://tieba.baidu.com/f?kz=1033300126
http://www.cnblogs.com/zgmf_x20a/archive/2008/11/15/1334109.html

用来求第k小数的,显然已知总点数的情况下可以用来求得第k大数。

但是后来发现第一种的版本是错的,

代码类似

	int ans=0;
	for(int i=1<<log2(all); i; i>>=1)
	{
		if(c[ans+i] <= k)
		{
			k-=c[ans+=i];
		}
	}
	return ans;

我因为这个WA了好久,有两个错误,①改成 if(c[ans+i] < k),最后return ans+1。理由同上面红字。 ② if 中还要加上 ans+i<all &&...


代码(两种方法合在一起,见注释):

//hdoj-4006,动态求第k大数
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

#define MAXN 1000005
#define LOWBIT(x) x&(-x)
int a[MAXN];		//原数组,最多1000000个数
int list[MAXN];	//排序数列
int q[MAXN];		//是否询问:Q
int c[MAXN];
int n, k, all;

int log2(int x) { int i=0; for(; x; x>>=1, i++); return i-1; }
void insert(int x)
{
	while(x<=all)
	{
		c[x]+=1;
		x+=LOWBIT(x);
	}
}
/////////////////////////////// 方法一:二进制,二分逼近
int find_kth_small(int k)
{
	int ans=0;
	for(int i=1<<log2(all); i; i>>=1)
	{
		if(ans+i<all && c[ans+i] < k)				//!!!
		{
			k-=c[ans+=i];
		}
	}
	return ans+1;
	/*													//这个一样
	int cnt=0, ans=0;
	for(int i=1<<log2(all); i; i>>=1)
	{
		ans+=i;
		if(ans>=all || cnt+c[ans]>=k) ans-=i;
		else cnt+=c[ans];
	}
	return ans+1;
	*/
}
int find_kth_great(int k, int tot)
{
	return find_kth_small(tot-k+1);
}
/////////////////////////////// 方法二:二分sum(i)
int sum(int i)
{
	int res=0;
	for(; i>0; i-=LOWBIT(i))
	{
		res+=c[i];
	}
	return res;
}

int search(int k)			//!!目标是 tot-sum(ans) < k <= tot-sum(ans-1),而普通的二分查找目标是k=a[ans].
{
	int l=1, r=all;
	int tot = sum(r);
	while(l<=r)
	{
		int mid = (l+r)/2;
		if(tot-sum(mid)>=k)
		{
			l = mid+1;
		}
		else if(tot-sum(mid)<k && k<=tot-sum(mid-1))
		{
			return mid;
		}
		else
		{
			r = mid-1;
		}
	}
	/*													//这个也是对的,但是我感觉上面的好理解
	while(l<=r)
	{
		int mid = (l+r)/2;
		if(tot-sum(mid-1)>=k) l=mid+1;
		else r = mid-1;
	}
	return l-1;
	*/
}

int main()
{
	while(cin>>n>>k)
	{
		memset(c, 0, sizeof(c));
		memset(q, 0, sizeof(q));
		all = 0;									//记录总共加入的数
		for(int i=0; i<n; i++)
		{
			char t[2];
			cin>>t;
			if(t[0] == 'I')
			{
				int x; cin>>x;
				a[i] = x;					//a[] 原数列
				list[all++] = x;		//加入的数列,之后要经过排序,方便散列
			}
			else if(t[0] == 'Q')
			{
				q[i] = 1;
			}
		}
		sort(list, list+all);
		int tot=0;
		for(int i=0; i<n; i++)
		{
			if(!q[i])
			{
				tot++;
				insert(lower_bound(list, list+all, a[i])-list+1);		//+1,避免0下标
			}
			else
			{
//				cout<<list[search(k)-1]<<endl;		//第一种,二进制二分逼近
				cout<<list[find_kth_great(k, tot)-1]<<endl; //第二种,二分sum(i)
			}
		}
	}
}


二、建堆

做一个小顶堆,放k个元素。每insert一个数,如果此数大于堆顶,就插入堆,并去掉原堆顶,如果小于则不操作,因为无影响。这个就是利用了这个题目的特殊性,即一个样例中k是固定不变的,不像其他题目可能会输入"Q k"这样作为一条指令。也因此造就了本场比赛第一水题。(掩面.....)

众所周知,优先队列(priority_queue)是用二叉堆实现的,下面给出STL优先队列的版本:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
struct cmp
{
	bool operator() (int a, int b) {return a<b? 0: 1;}
};
priority_queue<int, vector<int>, cmp> q;		//或者直接使用,greater<int>
int n, k;

int main()
{
	while(cin>>n>>k)
	{
		while(n--)
		{
			char t[2];	cin>>t;
			if(t[0] == 'I')
			{
				int x; cin>>x;
				if(q.size()<k) q.push(x);
				else if(x > q.top()) {q.pop(); q.push(x); }
			}
			else
			{
				cout<<q.top()<<endl;
			}
		}
	}
}

三、

待续...


你可能感兴趣的:(c,list,search,insert)