ACM第三次寒假集训

ACM第三次寒假集训

priority_queue

思路

利用优先队列的push()和pop()函数可以实现“insert”和"extract"取堆顶的元素

代码

#include<iostream>
#include<queue>
using namespace std;
int main(){
	priority_queue<int> p_q;
	string op;
	int num;
	while(1){
		cin>>op;
		if("insert"==op){
			cin>>num;
			p_q.push(num);
		}else if("extract"==op){
			cout<<p_q.top()<<endl;
			p_q.pop();
		}else if("end"==op) break;
	}
	return 0;
}

ST 表 && RMQ 问题

思路

先通过预处理得到每段区间的最大值,然后查询,但是如何能高效的预处理?

两个连续区间的最大值比较后得到的那个最最大值就是这两个区间连起来后的大区间的最大值,而不需要遍历一遍这个区间的所有数值。

任意两个相邻区间都可以组成一个新的大区间,但是怎么组成才能更加的有效率?长度为2的 j 次方的一段分成两段长度为2的 j-1 次方长度的两段合起来的区间就可以更快的表示某些区间的最值,但是这样并不能保证所有的区间都有结果,所以需要后面去凑结果。

任何一个十进制的整数都可以用一个二进制来表示,也就是可以表示为 (num)10=c0 * 2^0 + c1 * 2^1 + c2 * 2^2 + c3 * 2^3 + ...

其中 ci 取 0 或 1.但将区间的十进制拆成如上的方式需要log2(n)的时间,那么这里怎么优化?

考虑,真的有必要完全拆成一个二进制一步一步得出结果吗?

根据st表的定义原则,第二维字母 j 表示的步长为 2^j 所以如果是区间长度为 len 的区间的最值,我们可以在确定了左端点时找到步长来确定答案。但是 2^((int)log2(len)) <= len 所以只是dp[ i ] [ (int)log2(len) ]存储的结果也不足以证明是i ~ i+len-1这段的最值。但是分别以左、右端点为起点,以长度为 (int)log2(len) 长度分别向右向左延申得到的这个区间一定能代表 i ~ i+len-1 这个区间。

int len=r-l+1;
int j=(int)log2(len);//向下取整
int ans=Max(dp[l][j],dp[r-(1<<j)+1][j]);

代码

#include<bits/stdc++.h>
using namespace std;
int main(){
	int N,M;
	scanf("%d %d",&N,&M);
	int arr[N+1];
	int dp[N+1][(int)log2(N)+1];
	for(int i=1;i<=N;++i){
		scanf("%d",&arr[i]);
		dp[i][0]=arr[i];
	}
	for(int j=1;j<=log2(N);++j){
		for(int i=1;i+(1<<j)-1<=N;++i){
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
		}
	}
	for(int i=0;i<M;++i){
		int l,r;
		scanf("%d %d",&l,&r);
		int j=log2(r-l+1);
		printf("%d\n",max(dp[l][j],dp[r+1-(1<<j)][j]));
	}
	return 0;
} 

合并果子

思路

主要是利用哈夫曼树的性质,让出现频率多或者数值大的的树上路径长度尽可能的小,让出现次数少或者数值少的相对多就可以保证整棵树的 (求和)频率i * 长度i 的值尽可能的小

这道题同样可以利用优先队列得到当前所有果子堆里面数值最小的堆的值加起来,然后再把这堆的值放进优先队列的里面,重复这个动作

为什么要小根堆?因为两堆合并后还会放进队列里面,说明一开始的每一对不一定只出现一次,这对应的是哈夫曼编码每个字符出现的频率,那么先拿出来的后面很可能会多次叠加,也就是出现的频率比较高,这样的话,只有让数值尽可能的小,即使频率上去了,但每一次的数值比较小,也会使得最终耗费的体力值更小。

代码

#include<iostream>
#include<queue>
using namespace std;
int main(){
	priority_queue<int> p_q;
	int n,num,sum=0;
	cin>>n;
	int arr[n];
	for(int i=0;i<n;++i){
		cin>>num;
		p_q.push(-num);
	}
	while(1){
		int x,y;
		x=-p_q.top();p_q.pop();
		if(p_q.empty()) break;
		y=-p_q.top();p_q.pop();
		sum+=(x+y);
		p_q.push(-(x+y));
 	}
 	cout<<sum;
	return 0;
}

约瑟夫问题

思路

这道题可以通过队列的出队入队来模拟一个环得操作。

先按题目要求给存入n个编号分别为 1 ~ n,然后模拟报数。

内层循环为m-1次,执行出队和入队得操作,第m次操作只出队不如对,相当于从圈子力出去,然后持续这个循环直到队列为空

代码

#include<iostream>
#include<queue>
using namespace std;
int main(){
	queue<int> q;
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;++i) q.push(i);
	while(!q.empty()){
		for(int i=1;i<m;++i){
			int tem=q.front();
			q.pop();
			q.push(tem);
		}
		cout<<q.front()<<" ";q.pop();
	}
	return 0;
}

Look Up S

思路

运用单调递减栈,一旦有大于栈顶元素得要进入就开始出栈,并且出栈得都标记要入栈得这个元素得编号为仰望对象。最终还要进去一个INT_MAX来保证所有得进去得合理对象都会出来,但是此时应该令出去的标记其仰望对象为 ‘0’

代码

#include<iostream>
#include<climits>
#include<stack>
using namespace std;
struct bend{
	int num,it;
	bend(int n,int i):num(n),it(i){
	}
};
int main(){
	stack<bend> s;
	int n;
	cin>>n;
	int ans[n+1];
	s.push(bend(INT_MAX,0));
	for(int i=1;i<=n+1;++i){
		int num;
		if(i<=n) cin>>num;
		else num=INT_MAX;
		while(s.top().num<num){
			if(i<=n) ans[s.top().it]=i;
			else ans[s.top().it]=0;
			s.pop();
		}
		s.push(bend(num,i));
	}
	for(int i=1;i<=n;++i) cout<<ans[i]<<endl;
	return 0;
}

国旗计划

思路

从开始区间找到当前区间可以到的最远距离就可以保证尽可能用人少的情况下将所有战区经过一遍。但是还有一个信息是没有人的区域被其他战士套圈,也就是每个战士的负责地是一个压着一个并且不会被某一个战士完全覆盖,这样就保证这个区间里能出发的战士,在最右边得战士能够最大效率得覆盖战区这样将所有战士过一遍就可以得到最少需要几个战士来完成任务,时间为O(n) ,但是会询问n次,所以整个程序得时间复杂度为 O(n^2),包超时得老弟,所以有什么可以提高效率得办法吗?

有的,倍增思想。你说这是环,我咋倍增?我可以先将这个环看成一个链,这个环总共也就被走一圈多点,那我可以用一个长度为两倍环长度得链来表示整个行程,只需要把右端点得值小于左端点得右端点得值加上m,然后再把前 1~ n项在后面复制一下但是左右端点要同时+m。这样就变成了一条链。

怎么降低时间复杂度?我们有 2*n个人,从0开始编号每个人走太多步我们一时间不清楚,但我们可以尝试先得出n号战士负责得区间内那个战士从里面开始到得区间外最远的编号是多少,然后记录为 dp[ i ] [0]然后利用倍增的思想得到走更多是需要几号战士。

dp[i][j]=dp[dp[i][j-1]][j-1];

通过这样我们就可以通过dp[i] [j] 里面存的值来判断编号为i的战士按照用人最少策略来走圈,走过 2^j 次方个人后是几号战士。

预处理完毕,我们如何得到一共走了几步?我们最终的结果不一定是2的幂次,但是我们需要像第二题一样凑出来结果,

(num)10=c0 * 2^0 + c1 * 2^1 + c2 * 2^2 + c3 * 2^3 + ... 其中 ci 取 0 或 1

那么我们可以先找到2的次幂最大的那个是多少,因为次幂大一,可以满足超过 start+m的条件,但是可能次幂减一后就开始当前的右边界小于 start+m这个值,这说明我们找到了多项式和中2次幂最大的那项,然后我们继续从剩下的长度,新的起点战士开始再次找到原来第二大次幂的次幂,得到需要走的步数,一次类推,最终得到最下的次幂,但是这时候由于判断条件是要求右边界小于start+m所以这个时候的人数还是少一个,所以最终还需要再加 1

代码

#include<bits/stdc++.h>
using namespace std;
struct bend{
	int id,start,end;
	bool operator<(bend&b){
		return start<b.start;
	}
}mp[400001];
int dp[400001][20],ans[200001];
int n,m;
void pre(){
	for(int i=0,p=i;i<2*n;++i){
		while(p<2*n&&mp[p].start<=mp[i].end) ++p;//这里应当理解为什么p不是每次都从i开始,而可以继承上一次的结果 
		dp[i][0]=p-1;//理解这里为什么要-1,因为出循环的条件是左端点大于右边界 
	}
	for(int j=1;j<20;++j){
		for(int i=0;i<2*n;++i){
			dp[i][j]=dp[dp[i][j-1]][j-1];
		}
	}
}
void search(int k){
	int bound=mp[k].start+m,cnt=1,pos=k;
	for(int i=19;i>=0;--i){
		if(dp[k][i]!=0&&mp[dp[k][i]].end<bound){
			cnt+=(1<<i);
			k=dp[k][i];
		}//这里为什么不是直接退出+1而是更新当前基准值k?因为这里的i表示的是2的i次方,所以i-1不代表走的步数减一,所以要继续讨论 
	}
	ans[mp[pos].id]=cnt+1;//用贪心 
} 
int  main(){
	cin>>n>>m;
	for(int i=0;i<n;++i){
		cin>>mp[i].start>>mp[i].end;
		mp[i].id=i;
		if(mp[i].end<mp[i].start) mp[i].end+=m;
	}
	sort(mp,mp+n);//左闭右开所以是+n
	for(int i=0;i<n;++i){
		mp[i+n].id=mp[i].id;
		mp[i+n].start=mp[i].start+m;
		mp[i+n].end=mp[i].end+m;
	}//将环变成一条链
	//预处理
	 pre();
	 //搜寻结果
	for(int i=0;i<n;++i) search(i);
	for(int i=0;i<n;++i){
		cout<<ans[i];
		if(i!=n-1) cout<<" ";
	} 
	return 0;
}

学习总结

通过本次学习掌握了如何使用st表

预处理

for(int i=0;i<n;++i) dp[i][0]=**;//总之这里是只动一步的结果,其他结果都是以这一层为基础的。
for(int j=1;j<=log2(n);++j){
	for(int i=0;i+(1<<j)-1<n;++i){//右端点小于等于右边界
        dp[i][j]=f(i,j-1);
        //可以是max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
        //也可以是dp[i][j]=dp[dp[i][j-1]][j-1];
        //...还可能是其他形式,总之这里是预处理的操作
    }
}

访问

访问需要可以访问随机一段的dp值,可以将这段转成一个2次幂的多项式,然后逐项移动,将结果加起来。也可以利用最值可重复来得到两个会重叠的区间但是不影响结果的两个长区间取一次极值来获取结果。

posted @ 2025-02-07 21:50  Buy-iPhone  阅读(15)  评论(0)    收藏  举报