关于指针 – 一些O(n)的奇特技巧

综述

主要是双指针和滑动指针技巧

双指针

双指针一般用于序列问题,其分配两个指针来完成算法任务,可将问题优化为线性复杂度

常见场景及应用如下

  • 两边向内型
    ①快速排序的前后指针互换
    ②两头指针内夹反转序列
    ③指针位置限定排序序列大小关系
  • 同向异行型
    ①慢指针等待快指针结果
    ②步长不同的双指针找分点
    ②步长不同的双指针判环
  • 保持一致型
    ①序列一一比对
    ②归并排序的双指针筛数
    ③指针用来抵偿达到某种一致

PAT-A1085 排序数组连续序列

PAT-A1089

PAT-A1029 寻找中间数

最简单的做法是把两个序列读入同一个vector,调sort()函数,索引$\scriptsize \lfloor N/2\rfloor $即可,复杂度 $\scriptsize O(nlogn)$
鉴于本题样例规模 $\scriptsize \sim 10^5$,考虑类似归并有序序列的 $\scriptsize O(n)$ 双指针方法
基于有序列表的性质,还可用二分方法优化至 $\scriptsize O(logn)$,将在二分文章中讲解


(ˉ﹃ˉ) Q:两个数组都可能会优先被遍历完末尾,这种情况怎么处理?

(′~`●) A:任一指针先到达末尾,后续都只能从另一序列继续递增。基于此想法,优先处理到达末尾S[n-1]的情况:排除到达末尾情况,此时两指针的递增保证在合法范围,就可以类似合并有序序列操作啦

(?∀?) Q:如果p1,p2初始化为0,假定S1<S2,在刚刚遍历完毕S1从S2开始时,p2++会多加一次,不加又无法向后继续怎么办?

(-ω- ) A:呼~动下小脑筋,让指针从 -1开始递增 ,是不是就能统一啦?相应地,在比较元素时,必须比较的是 S1[p1+1]和S2[p2+1]。当然也有个指针从0开始的好方法,就是让结尾S[n]=MAX_INT,这样也达到了统一

(-_-。)??? Q:怎么判定算法结束?怎么判定中位数最后是S1[p1]还是S2[p2]?

(`・ω・′)ゞ A:循环次数实际就是指针移动的次数,将两个序列看为一个序列,中位数下标为 $\scriptsize (\frac{N-1}{2})=(\frac{n1+n2-1}{2})$,指针从 $\scriptsize (-1)→(\frac{n1 + n2 – 1}{2})$ 得循环次数为 $\scriptsize \frac{n1 + n2 + 1}{2}$;最后一次修改后的指针必然是所求中位数位置,每次修改指针使用变量保存起来就OK了。使用0开始方案可以减少访存:中位数实际是结束时p1和p2所指的元素中更小的那个

(●?ω?●) Q:题目加黑的long int,我要设置long类型吗?

(◍’౪`◍)ノ゙ A:g++中long int与int同被编译为4B=32bit,int即可


#include <iostream>
using namespace std;

const int maxn = 200001;
int s1[maxn], s2[maxn];

int main() {
    int n1, n2;
    scanf("%d", &n1);
    for (int i = 0; i < n1; i++)
        scanf("%d", &s1[i]);
    scanf("%d", &n2);
    for (int i = 0; i < n2; i++)
        scanf("%d", &s2[i]);

    int p1 = -1, p2 = -1, cnt = (n1 + n2 + 1) / 2, m;
    while (cnt--) {
        if (p1 == n1 - 1) { m = s2[++p2]; continue; }
        if (p2 == n2 - 1) { m = s1[++p1]; continue; }
        if (s1[p1 + 1] < s2[p2 + 1]) m = s1[++p1];
        else m = s2[++p2];
    }
    printf("%d\n", m);
    return 0;
}

PAT-A1048 排序数组连续和问题

关于此问题及变式解法如下

和为S的两数
给定两指针:$\scriptsize small$ 从0开始后向遍历,$\scriptsize large$ 从N-1开始前向遍历
① 先卡右边界确定最大能取的数:此时 $\scriptsize small$ 是最小的,和 $\scriptsize sum \gt small+large$,一定是 $\scriptsize large$ 过大要向小调整,否则无解。
② 再卡左边界最小可取的数:此时 $\scriptsize large$ 已经是最大了,如果这个时候 $\scriptsize sum \lt small+large$,说明 $\scriptsize small$ 还不够大要向大调整,否则无解。
③ $\scriptsize sum = small+large$,此时找到最小 $\scriptsize small$ 和最大 $\scriptsize large$ 的解,若要寻找所有解,只可能在范围 $\scriptsize [small+1,large-1] 中$

连续和S的子序列
思想同上
$\scriptsize \sum\limits_{i=small}^{large}a[i] \gt S$,large–,
$\scriptsize \sum\limits_{i=small}^{large}a[i] \lt S$,large++
$\scriptsize \sum\limits_{i=small}^{large}a[i] == S$,small++,large–

#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;

const int maxn = 100001;
int coins[maxn];

int main() {
    int N, M;
    scanf("%d %d", &N, &M);
    memset(coins, 0, sizeof(coins));
    for (int i = 0; i < N; i++)
        scanf("%d", &coins[i]);

    sort(coins, coins + N);
    int small = 0, large = N - 1;
    while (small < large) {
        if (coins[small] + coins[large] > M)
            large--;
        else if (coins[small] + coins[large] < M)
            small++;
        else {
            printf("%d %d\n", coins[small], coins[large]);
            return 0;
        }
    }

    printf("No Solution\n");
    return 0;
}

滑动指针

滑动指针的思想是不计算具体值,只给边界并按照一定规律移动

PAT-A1105 螺旋矩阵

确定螺旋方向顺序是“右下左上”:
实现① 左右加减col,上下加减row
实现② 上下左右记入数组 $\scriptsize D=[[0,1],[1,0],[0,-1],[-1,0]]$

确定何时换方向:每检测到边界换下一方向
实现①通过while保持每个方向不变直到下一边界,防止中途更改方向
实现②检测到边界变更方向 $\scriptsize row + D[(++i)\%4][0],col+D[(++i)\%4][1]$

关于边界的确定:
① 访问标记:不超数组边界、不覆盖已访问并标记的元素
② 逐层内夹:不超过当轮四个最外层号记录变量


Q:实现①我的while全写的是if,感觉没什么问题啊

A:不坚持一个方向的话,会在中途匹配到符合条件的其他方向,如下样例一直到53还运行正常,但是下一个匹配的就是20了。所以每到边界才能变更方向,变更后直到下一个边界前都要保持方向

98 95 93
42 37 81
53 20 76
58 60 76

Q:啊……我的逻辑完全没问题,但还是有测试点错误?

A:看看你的数据满不满足 $\scriptsize m\geqslant n$,题中着重要求啦。题意的最小差距是整数算数平方根,找到 $\scriptsize m=\lfloor\sqrt N\rfloor$,若有整数算术平方根,m即所求;无整数算数平方根时递增寻找第一个可被整除的m,所得两因子差值最小。递减求n也可,尤其是大质数样例可减少遍历次数,但要注意求n时需要判别并保证与m的大小关系


#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;
int num[100001];

int main() {
    int N, m = 1, n;
    scanf("%d", &N);
    for (int i = 0; i < N; i++)
        scanf("%d", &num[i]);
    sort(num, num + N, greater<int>());

    for (; m * m < N; m++);
    while(N % m != 0) m++;
    n = N / m;

    int ans[m][n];
    memset(ans, -1, sizeof(ans));
    int row = 0, col = -1, p = 0;
    while (p < N) {
        while (col < n - 1 && ans[row][col + 1] == -1) ans[row][++col] = num[p++];
        while (row < m - 1 && ans[row + 1][col] == -1) ans[++row][col] = num[p++];
        while (col > 0 && ans[row][col - 1] == -1) ans[row][--col] = num[p++];
        while (row > 0 && ans[row - 1][col] == -1) ans[--row][col] = num[p++];
    }

    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            if (j < n - 1) printf("%d ", ans[i][j]);
            else printf("%d\n", ans[i][j]);

    return 0;
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号