算法日记Day2---【题解C++】P4155 [SCOI2015] 国旗计划

#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;
}
相关推荐
BeyondESH7 分钟前
Linux线程同步—竞态条件和互斥锁(C语言)
linux·服务器·c++
青椒大仙KI1112 分钟前
24/9/19 算法笔记 kaggle BankChurn数据分类
笔记·算法·分类
^^为欢几何^^16 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
豆浩宇16 分钟前
Halcon OCR检测 免训练版
c++·人工智能·opencv·算法·计算机视觉·ocr
WG_1725 分钟前
C++多态
开发语言·c++·面试
浅念同学32 分钟前
算法.图论-并查集上
java·算法·图论
何不遗憾呢41 分钟前
每日刷题(算法)
算法
立志成为coding大牛的菜鸟.1 小时前
力扣1143-最长公共子序列(Java详细题解)
java·算法·leetcode
鱼跃鹰飞1 小时前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
liangbm31 小时前
数学建模笔记——动态规划
笔记·python·算法·数学建模·动态规划·背包问题·优化问题