#P4155 [SCOI2015] 国旗计划
这道题对我来说难度确实高了些,是我在学习倍增法 时碰到的一道例题。剖去新知识倍增法,此题还涉及到了贪心、断环为链的思想------这些都是我已经学过的,因此写下这篇博客,体会新知识的同时,温习旧知识。
本题可以简单描述为:
边境上有m个边防站围成一圈,顺时针 编号为1~m。有n名战士,每名战士常驻两个站,能在两个站之间移动。现在局长有一个"国旗计划",让边防战士举着国旗环绕一圈 。局长想知道至少需要多少战士才能完成"国旗计划",并且他想知道在某个战士必须参加的情况下,至少需要多少名边防战士。
输入:第一行,包含两个正整数N,M,分别表示边防战士数量和边防站数量。随后N行,每行包含两个正整数。其中第i行包含的两个正整数Ci、Di分别表示i号边防战士常驻的两个边防站编号,Ci号边防站沿顺时针方向至Di号边防站的奔袭区间。数据保证整个边境线都是可被覆盖的。
一、处理输入------断环为链
既然边防士兵都围成了一个圈 ,那么边防士兵的奔袭区间就有可能 <math xmlns="http://www.w3.org/1998/Math/MathML"> C i > D i Ci>Di </math>Ci>Di,因为沿着圈顺时针运动嘛 。而且根据题意,每一个士兵都作为一个起点,都要绕一个圈。 那么相比于圈这样的逻辑结构,我们更熟悉链式的/线性的逻辑结构,因为这样处理起来方便。
不过此时可能会有小伙伴说,在我们学习用数组实现循环队列 时,逻辑结构同样像一个圆,那为什么那个时候我们使用的是模运算,而不是方便操作的断环为链呢?
其实这就要具体情况具体分析了。模运算在处理"圆"问题方面有优势,但是在实现循环队列的是时候,我们考虑的主要是数据的增删,而数组不可能开得无限大 ,因此就需要模运算来实现"循环" 的同时节省空间了。
而本题,并没有什么增删操作,而更多的是查找和计算,并且考虑到之后倍增法 的使用,选择牺牲一部分空间来提升查询效率的断环为链为更合适(还有疑问的话,可以再往下看看)
cpp
//基本数据定义
struct Warrior
{
/* data */
int id;//边防战士的编号
int L,R;//顺时针活动的范围/负责的 边防站编号
bool operator < (const Warrior b)const{
return L<b.L;
}
};
Warrior wr[N*2];//化简看来,每个边防战士,就是一个个区间
scanf("%d%d",&n,&m);//m就是该圆的周长
for(int i=1;i<=n;i++){
wr[i].id=i;
scanf("%d%d",&wr[i].L,&wr[i].R);
if(wr[i].R<wr[i].L) wr[i].R+=m;//把环变成链
}
sort(wr+1,wr+n+1);//按左端点排序。因为据题意,各区间不会有包含关系,因此右端点也是递增的
n2=n;
//断环为链的核心代码部分
for(int i=1;i<=n;i++){//把环,复制,加倍伸长为一条链
n2++;
wr[n2]=wr[i];
wr[n2].L=wr[i].L+m; wr[n2].R=wr[i].R+m;
}
成链的过程和结构直观来看就应该是这样的:
二、倍增法------预处理倍增数组
简单介绍一下倍增法:
倍增就是"成倍增长"。利用二进制本身的倍增特性, 把一个数N用二进制展开,可以观察出二进制数一种快速增长的特性,即简单地操作一位就可以对整体有明显的增减影响 。一个整数 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,它的二进制只有 <math xmlns="http://www.w3.org/1998/Math/MathML"> l o g 2 n log₂n </math>log2n位。如果要从0增长到 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,可以以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 , 2 , 4 , . . . . . , 2 k 1,2,4,.....,2k </math>1,2,4,.....,2k为"跳板",快速跳到 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n------这样的跳跃次数很少( <math xmlns="http://www.w3.org/1998/Math/MathML"> l o g 2 n log₂n </math>log2n个),就足以让0跃升到 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n。
那么在本题中,谁是那个最开始的数,谁又是"跳板" 呢?
根据题意,我们可以将每一个边防士兵都抽象成一个个不相包含的区间 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( L , R ) (L,R) </math>(L,R),国旗在不同的区间被传递,相当于每一个区间/边防士兵就是一个个跳板------国旗从起点借助一个个区间抵达终点。
cpp
const int N=4e5+5;
int go[N][20];//go[s][i]表示从起点s跳2^i步抵达的区间
接下来让我们详细了解一下倍增数组: <math xmlns="http://www.w3.org/1998/Math/MathML"> g o [ s ] [ i ] go[s][i] </math>go[s][i]表示从第 <math xmlns="http://www.w3.org/1998/Math/MathML"> s s </math>s个区间出发,走 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2ⁱ </math>2i个最优区间后到达的区间 。预计算出所有的区间出发的 <math xmlns="http://www.w3.org/1998/Math/MathML"> g o [ ] [ ] go[][] </math>go[][],以它们为"跳板",就能快速跳到目的地。以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 n 2ⁿ </math>2n的方式来增长,那么它的递推公式应该是这样的:
cpp
for(int i=1;(1<<i)<=n;i++){
for(int s=1;s<=n2;s++){//s就代表每一个区间
//现开始从每个区间经过倍增,可以抵达的合理且最远的下一区间
go[s][i]=go[go[s][i-1]][i-1];
//先从起点s跳2i-1步到区间z=go[s][i-1]
//再从z跳2i-1步跳到区间go[s][i]
}
}
应该没人疑惑为什么是跳 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i − 1 2ⁱ⁻¹ </math>2i−1步而不直接是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 i 2ⁱ </math>2i步吧?因为递推公式嘛,肯定是要联系到前一项的。 另外题目要求至少需要多少边防战士,那也就意味着,每一名战士都要去到尽可能远的边防站,也就应该用到贪心。 可以认真体会以下代码:
cpp
//预处理倍增数组
//先对所有区间排序
sort(wr+1,wr+n+1);//按左端点排序。因为据题意,各区间不会有包含关系,因此右端点也是递增的
void init(){//贪心 + 倍增
int next=1;
for(int i=1;i<=n2;i++){//对链上的每一个区间都做处理
while(next<=n2 && wr[next].L<=wr[i].R) next++;
go[i][0]=next-1;
}
//加一层抽象
for(int i=1;(1<<i)<=n;i++){//倍增,枚举跳板
for(int s=1;s<=n2;s++){//s就代表每一个区间
//现开始从每个区间经过倍增,可以抵达的合理且最远的下一区间
go[s][i]=go[go[s][i-1]][i-1];
//这个递推和遍历顺序是很合理的
//因为即便是跳2i-1步仍然到不了下一个区间,递推也不会出错,得到的值仍然是当前的区间
}
}
}
三、倍增法------计算每个战士
理解了以上部分,来算每个战士的最少需要数量就容易多了。只需要枚举跳跃次数,然后检查它与终点的关系就好了。
怎样枚举?从最大的跳跃步数 <math xmlns="http://www.w3.org/1998/Math/MathML"> l o g 2 ( N ) log₂(N) </math>log2(N)开始枚举就好了,这样既可以保证每个战士都能到达其对应的终点,也能在某种程度上以贪心的思想优化时间复杂度。
怎样检查?很简单,就是检查当前位置和它对应终点的大小关系,"断环为链"的妙处又体现出来了:
cpp
void getAns(int x){
int len=wr[x].L+m,cur=x,ans=1;
for(int i=log2(N);i>=0;i--){
//从最大的i开始找
int pos=go[cur][i];
if(pos && wr[pos].R<len){//wr[pos].R>l=len,跳得太过了,已经超出了x边防员的行动范围
ans += 1<<i;//累加跳过的区。区间就是边防员
cur=pos;//从新位置开始
}
}
res[wr[x].id]=ans+1;
}
结语
本题的关键就是断环为链 ,和倍增法两部分,其中倍增数组go的预处理和使用成了最重要同时也是最难理解的部分,值得好好品味。
源代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5;
int n,n2,m;
struct Warrior
{
/* data */
int id;//边防战士的编号
int L,R;//顺时针活动的范围/负责的 边防站编号
bool operator < (const Warrior b)const{
return L<b.L;
}
};
Warrior wr[N*2];//化简看来,每个边防战士,就是一个个区间
int go[N][20];//go[s][i]表示从起点s跳2^i步抵达的区间
int res[N];
//预处理倍增数组
void init(){//贪心 + 倍增
int next=1;
for(int i=1;i<=n2;i++){//对链上的每一个区间都做处理
while(next<=n2 && wr[next].L<=wr[i].R) next++;
go[i][0]=next-1;
}
//加一层抽象
for(int i=1;(1<<i)<=n;i++){
for(int s=1;s<=n2;s++){//s就代表每一个区间
//现开始从每个区间经过倍增,可以抵达的合理且最远的下一区间
go[s][i]=go[go[s][i-1]][i-1];
}
}
}
void getAns(int x){
int len=wr[x].L+m,cur=x,ans=1;
for(int i=log2(N);i>=0;i--){
//从最大的i开始找
int pos=go[cur][i];
if(pos && wr[pos].R<len){//wr[pos].R>l=len,跳得太过了,已经超出了x边防员的行动范围
ans += 1<<i;//累加跳过的区。区间就是边防员
cur=pos;//从新位置开始
}
}
res[wr[x].id]=ans+1;
}
int main()
{
scanf("%d%d",&n,&m);//m就是该圆的周长
for(int i=1;i<=n;i++){
wr[i].id=i;
scanf("%d%d",&wr[i].L,&wr[i].R);
if(wr[i].R<wr[i].L) wr[i].R+=m;//把环变成链
}
sort(wr+1,wr+n+1);//按左端点排序。因为据题意,各区间不会有包含关系,因此右端点也是递增的
n2=n;
for(int i=1;i<=n;i++){//把环,复制,加倍伸长为一条链
n2++;
wr[n2]=wr[i];
wr[n2].L=wr[i].L+m; wr[n2].R=wr[i].R+m;
}
init();
for(int i=1;i<=n;i++) getAns(i);//计算每个战士
for(int i=1;i<=n;i++) printf("%d ",res[i]);
return 0;
}