双指针+BFS+图论

这里是目录

  • 双指针+BFS+图论
    • 双指针
      • 日志统计
        • 暴力做法
        • 双指针优化
    • BFS
      • 献给阿尔吉侬的花束
    • 图论
      • 交换瓶子
        • 暴力做法
        • 置换群算法

双指针+BFS+图论

双指针

日志统计

来源:第九届蓝桥杯省赛C++B组,第九届蓝桥杯省赛JAVAB组

小明维护着一个程序员论坛。现在他收集了一份”点赞”日志,日志共有 N 行。

其中每一行的格式是:

ts id  
表示在 ts 时刻编号 id 的帖子收到一个”赞”。

现在小明想统计有哪些帖子曾经是”热帖”。

如果一个帖子曾在任意一个长度为 D 的时间段内收到不少于 K 个赞,小明就认为这个帖子曾是”热帖”。

具体来说,如果存在某个时刻 T 满足该帖在 [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是”热帖”。

给定日志,请你帮助小明统计出所有曾是”热帖”的帖子编号。

输入格式
第一行包含三个整数 N,D,K。

以下 N 行每行一条日志,包含两个整数 ts 和 id。

输出格式
按从小到大的顺序输出热帖 id。

每个 id 占一行。

数据范围
1≤K≤N≤105,
0≤ts,id≤105,
1≤D≤10000
输入样例:
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
输出样例:
1
3

这题刚看的时候可能没啥思路,可以试一下暴力

暴力做法

最容易想到的暴力解法
第一层循环是 时间 0到最大时间
第二层循环是 所有的id 判断它的时刻是否属于要求的长度的时间区间
是的话就统计它的次数 +1
然后判断一下它的次数是否已经>k 是的话将布尔数组它的位置 置为true
然后再每次判断新的时间区间时 要将cnt初始化为全0

暴力伪代码

for (时间段) {
    Arrays.fill(cnt, 0); // 数组初始化为0
    for (id) {
        cnt[id]++;
        if (cnt[id] >= k) st[id] = ture; // 如果是热帖 标记为true
    }
}

暴力代码:

#include 

#define ts first
#define id second

using namespace std;

const int N = 1e5+10;


typedef pair<int,int> pii;

pii blog[N];
bool hot[N];
int cnt[N];
int n,d,k;

int main(){
    scanf("%d%d%d",&n,&d,&k);
    int maxt = 0;
    for(int i = 1;i<=n;i++)
    {
        scanf("%d%d",&blog[i].ts,&blog[i].id);
        maxt = max(blog[i].ts,maxt);
    }

    for(int time = 0;time <= maxt; time++)
    {
	    memset(cnt,0,sizeof(cnt));
	    for(int i = 1;i<=n;i++)
	    {
	        int t = blog[i].ts;
	        int id = blog[i].id;
	        if( t >= time && t <time+d)
	            cnt[id]++;
	        if(cnt[id] >= k)
	            hot[id] = true;
	    }
    }
    for(int i = 0;i<=1e5;i++)
    if(hot[i]) printf("%d\n",i);

}

双指针+BFS+图论_第1张图片
但是这样只能过两个数据,我们再想一下怎么优化。

双指针优化

在我们枚举时间段时,会发现有很大一部分是重复的:
双指针+BFS+图论_第2张图片
假设我们已经统计了上面区间的id次数,那我统计下面区间id次数,我们可以直接把开头去掉,把结尾加上即可:
双指针+BFS+图论_第3张图片
即:

cnt[id[i]]--,cnt[id[j]]++
for (时间段) {
    cnt[id[i]]--; // 减去开头
    cnt[id[j]]++; // 加上结尾
}

时间复杂度优化成了O(N)
排序+双指针

① 先将所有的点赞数量按照时间顺序排好

② 通过双指针i和j维护长度不大于d的区间,并记录该区间的中所有帖子获得的赞数

下面请看代码:

#include 

#define ts first
#define id second

using namespace std;

const int N = 1e5+10;

typedef pair<int,int> pii;

pii blog[N];
bool hot[N];
int cnt[N];
int n,d,k;

int main(){
    scanf("%d%d%d",&n,&d,&k);

    for(int i = 0;i<n;i++) //因为要用到sort  所以下标从0 开始
        scanf("%d%d",&blog[i].ts,&blog[i].id);

    sort(blog,blog+n);

    for(int i = 0,j = 0; i < n;i++)
    {
        int bid = blog[i].id;
        cnt[bid]++;
        while(blog[i].ts - blog[j].ts >= d)
        {
            cnt[blog[j].id]--;
            j++;
        }
        if(cnt[bid] >= k) hot[bid] = true;
    }

    for(int i = 0;i<=1e5;i++)
        if(hot[i]) printf("%d\n",i);
}

双指针+BFS+图论_第4张图片
这样数据就都能过了。

BFS

之前写过DFS,现在再来讲一下BFS,其实这俩就对应了栈和队列,先看图解。
双指针+BFS+图论_第5张图片
画得不太好,差不多就是这样的,下面请看题。

献给阿尔吉侬的花束

阿尔吉侬是一只聪明又慵懒的小白鼠,它最擅长的就是走各种各样的迷宫。

今天它要挑战一个非常大的迷宫,研究员们为了鼓励阿尔吉侬尽快到达终点,就在终点放了一块阿尔吉侬最喜欢的奶酪。

现在研究员们想知道,如果阿尔吉侬足够聪明,它最少需要多少时间就能吃到奶酪。

迷宫用一个 R×C 的字符矩阵来表示。

字符 S 表示阿尔吉侬所在的位置,字符 E 表示奶酪所在的位置,字符 # 表示墙壁,字符 . 表示可以通行。

阿尔吉侬在 1 个单位时间内可以从当前的位置走到它上下左右四个方向上的任意一个位置,但不能走出地图边界。

输入格式
第一行是一个正整数 T,表示一共有 T 组数据。

每一组数据的第一行包含了两个用空格分开的正整数 R 和 C,表示地图是一个 R×C 的矩阵。

接下来的 R 行描述了地图的具体内容,每一行包含了 C 个字符。字符含义如题目描述中所述。保证有且仅有一个 S 和 E。

输出格式
对于每一组数据,输出阿尔吉侬吃到奶酪的最少单位时间。

若阿尔吉侬无法吃到奶酪,则输出“oop!”(只输出引号里面的内容,不输出引号)。

每组数据的输出结果占一行。

数据范围
1

先看图解:
双指针+BFS+图论_第6张图片
下面再请看代码:

#include
#include
#include
#include
#include

#define x first
#define y second

using namespace std;

const int N = 210;

typedef pair<int, int> PII;

int n, m;
char g[N][N];
int dist[N][N];  // 把判重和距离数组合为一个

int bfs(PII start, PII end)  // 注意 start end都是PII类型的
{
    queue<PII> q;

    memset(dist, -1, sizeof dist);  // 把距离数组都初始化成-1

    dist[start.x][start.y] = 0;  // 起点开始,距离为0

    q.push(start);  // 起点 入队

    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//遍历四个方向

    while(q.size())
    {
        PII t = q.front();

        // 弹出队头
        q.pop();

        for (int i = 0; i < 4; i ++ )
        {
            int a = t.x + dx[i], b = t.y + dy[i]; 

            if (a < 0 || a >= n || b < 0 || b >= m) continue;  // 出界,跳出本次循环
            if (dist[a][b] != -1) continue;    // 走过了,跳出本次循环
            if (g[a][b] == '#') continue;    // 撞到障碍物

            dist[a][b] = dist[t.x][t.y] + 1;

            if (end == make_pair(a, b)) return dist[a][b];  // 走到终点了,返回距离

            q.push({a, b});
        }

    }

    return -1;
}

int main()
{
    int T;
    cin >> T;

    while(T --)
    {

        scanf("%d%d", &n, &m);

        for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);

        PII start, end;
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ )
                if (g[i][j] == 'S') start = {i, j};
                else if (g[i][j] == 'E') end = {i, j};

        int distance = bfs(start, end);

        if (distance == -1) printf("oop!\n");
        else printf("%d\n", distance);

    }

    return 0;
}

图论

交换瓶子

有 N 个瓶子,编号 1∼N,放在架子上。

比如有 5 个瓶子:

2 1 3 5 4
要求每次拿起 2 个瓶子,交换它们的位置。

经过若干次后,使得瓶子的序号为:

1 2 3 4 5
对于这么简单的情况,显然,至少需要交换 2 次就可以复位。

如果瓶子更多呢?你可以通过编程来解决。

输入格式
第一行包含一个整数 N,表示瓶子数量。

第二行包含 N 个整数,表示瓶子目前的排列状况。

输出格式
输出一个正整数,表示至少交换多少次,才能完成排序。

数据范围
1≤N≤10000,

输入样例1:
5
3 1 2 5 4
输出样例1:
3
输入样例2:
5
5 4 3 2 1
输出样例2:
2
暴力做法

算法思路
1、通过观察可以发现,我们每一个数都必须回到它自己的位置上,比如 1 必须在第一位,2 必须在第二位上

2、那么我们就可以这样操作,由于每个数必须回到自己的位置,直接从 1 枚举到 nn ,如果当前位置的数不等于它的下标,那么我们就必须要把它给替换掉

3、设当前位置为 i 的话,那么我们就从 i+1 开始往后枚举,直到找到对应的 a[j] 和我们的 i 相等,那么我们就把上个数交换,把交换次数++

4、容易证明这个算法的正确性,由于每个数必须回到原来的位置,所以我们这样子操作不会出现多于的步骤,因为每次操作都是必须的,即使这个算法看起来很麻烦

#include

using namespace std;

const int N=10010;

int n;

int a[N];

int main()
{
    scanf("%d",&n);

    for(int i=1;i<=n;i++)scanf("%d",&a[i]);

    int sum=0;
    for(int i=1;i<=n;i++)
    {
        if(a[i]!=i)//直接遍历,如果不是自身的话,我们必然要交换,所以不会出现多于的操作
        {
            for(int j=i+1;j<=n;j++)
            if(a[j]==i)
             swap(a[i],a[j]);

             sum++;
        }
    }

    printf("%d\n",sum);

    return 0;
}

置换群算法

双指针+BFS+图论_第7张图片
双指针+BFS+图论_第8张图片

情况:
1. 一个环中的两个节点交换–> 分裂成两个
2. 两个环中的两个节点交换–> 合成一个环
k个环–> n个环 (自环)
最少需要 n-k个操作, 所以答案就是n-k

#include

using namespace std;

const int N = 1e4 + 5;

int n;
int b[N];
bool st[N];

int main() {
    
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> b[i];
    }
    int cnt = 0; //环的数量
    for (int i = 1; i <= n; i++) {
        if (!st[i]) { //如果没有标记过, 说明这个点在一个新的环中
            cnt++; //环++
            // 把这个点能到的所有点全部标记一下
            // 1 2 3 4 5
            // 3 1 2 5 4
            // 3指向2  j=b[3]=2
            // 2指向1  j=b[2]=1
            // 1指向3  j=b[1]=3;
            // 3指向2  j=b[3]=2 -->被标记过了
            // s[2] s[3]
            // s[4] s[4]=true  4指向5 j=b[4]=5
            //      s[5]=true  5指向4 j=b[5]=4;
            // 一共两个环
            for (int j = i; !st[j]; j = b[j]) {//走完这个环 每次变更指向b[j] 即瓶子初始序号的第j个
                st[j] = true;
            }
        }
    }
    cout << n - cnt << endl;
    return 0;
}

你可能感兴趣的:(算法与数据结构,经验分享,算法,数据结构)