第一章 基础算法(1)

目录

  • 1 快速排序算法
    • 1.1 快速排序算法
    • 1.2 查找数组中第k个数
  • 2 归并排序算法
    • 2.1 归并排序算法
    • 2.2 超快速排序 (逆序对)
    • 2.3 奇数码问题 (逆序对+奇偶性)
  • 3 二分查找算法
    • 3.1 二分查找算法模板
    • 3.2 数的范围
    • 3.3 数的三次方根(浮点数的二分)
    • 3.4 倒垃圾
    • 3.5 最佳牛围栏(求一个平均数最大的、长度不小于 L L L 的连续子段)
    • 3.6 特殊排序
    • 3.7 找出第 K 小的数对距离
    • 3.8 防线
    • 3.9 赶牛入圈
  • 4 高精度计算
    • 4.1 高精度加法
    • 4.2 高精度减法
    • 4.3 高精度乘法
    • 4.4 高精度除法
    • 4.5 数的进制转换
  • 5 前缀和与差分
    • 5.1 前缀和
    • 5.2 子矩阵的和——二维前缀和
    • 5.3 一维差分数组
    • 5.4 求差分矩阵——二维差分
    • 5.5 激光炸弹
    • 5.6 增减序列
    • 5.7 最高的牛
    • 5.8 最长平衡串
  • 6 双指针算法
    • 6.1 最长连续不重复子序列
    • 6.2 数组元素的目标和
    • 6.3 判断子序列
    • 6.4 统计得分小于 K 的子数组数目
    • 6.5 盛最多水的容器
    • 6.6 颜色分类 (三数排序)
    • 6.7 最小面积子矩阵
    • 6.8 最大的和
  • 7 位运算
    • 7.1 求二进制中1的个数
    • 7.2 位操作练习
    • 7.3 二进制数
    • 7.4 a^b
    • 7.5 64位整数乘法
    • 7.6 最短Hamilton路径
    • 7.7 起床困难综合症
    • 7.8 只出现一次的数字 I
    • 7.9 只出现一次的数字 II
    • 7.10 只出现一次的数字 III
    • 7.11 求整数二进制表示下所有是 1 1 1的位
  • 8 离散化
    • 8.1 区间和
    • 8.2 电影 (排序+离散化)

在这个模板中,快排、归并、二分、高精度存储数据的下标都可从0开始,前缀和和差分从1开始。

1 快速排序算法

1.1 快速排序算法

ACWing 785

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, a[N];

void quick_sort(int q[], int l, int r) {
    if (l >= r) return;
    int i = l - 1, j = r + 1, x = q[(l + r) >> 1];
    while (i < j) {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    quick_sort(a, 0, n - 1);
    for (int i = 0; i < n; i++) printf("%d ", a[i]);
    puts("");
    return 0;
}

1.2 查找数组中第k个数

ACWing 786

时间复杂度: O ( n ) O(n) O(n)

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, k, a[N];

int quick_sort(int q[], int l, int r, int k) {
    if (l >= r) return q[l];
    int i = l - 1, j = r + 1, x = q[(l + r) >> 1];
    while (i < j) {
        do i++; while (q[i] < x);
        do j--; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    if (j - l + 1 >= k) quick_sort(q, l, j, k);
    else quick_sort(q, j + 1, r, k - (j - l + 1));
}

int main() {
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    int ret = quick_sort(a, 0, n - 1, k);
    printf("%d\n", ret);
    return 0;
}

2 归并排序算法

2.1 归并排序算法

ACWing 787

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, nums[N], tmp[N];

void merge_sort(int q[], int l, int r) {
    if (l >= r) return;
    int mid = (l + r) >> 1;
    merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    for (int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", nums + i);
    merge_sort(nums, 0, n - 1);
    for (int i = 0; i < n; i++) printf("%d ", nums[i]);
    puts("");
    return 0;
}

2.2 超快速排序 (逆序对)

ACwing 107

如果相邻两个数满足 a i < a i + 1 a_i < a_{i+ 1} ai<ai+1,交换这相邻两个数只会让所有的逆序对数量减 1 1 1,对其余逆序对没有任何影响。因此如果有 k k k对逆序对,则至少要操作 k k k次。

最少需要进行操作的数量 = 所有逆序对数量

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 5e5 + 10;

int n;
LL q[N], tmp[N];

LL merge_sort(int l, int r) {
    if (l >= r) return 0;
    int mid = (l + r) >> 1;
    LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else {
            res += mid - i + 1;
            tmp[k++] = q[j++];
        }
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    for (int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
    return res;
}

int main() {
    while (scanf("%d", &n), n) {
        for (int i = 0; i < n; i++) scanf("%lld", q + i);
        printf("%lld\n", merge_sort(0, n - 1));
    }
    return 0;
}

2.3 奇数码问题 (逆序对+奇偶性)

ACwing 108

两个性质:逆序对+奇偶性

题解参考:题解1,题解2

算法思路:首先我们将这个 n 2 n^2 n2的矩阵从二维降低到一维,也就是开一个 n 2 n^2 n2的一维数组,然后抛去 0 0 0这个空格计算逆序对个数,如果两个矩阵的逆序对的奇偶性相同的话,就可以输出TAK。

证明还没有,y总没讲这个题

代码中的注释:在代码中 m e r g e ( 1 , n × n ) merge(1, n \times n) merge(1,n×n)有个细节,因为我们输入的时候实际上仅输入了 n × n − 1 n \times n - 1 n×n1个数, 没有输入空格 0 0 0。但是这里要多考虑一位 0 0 0,实际上的逆序对数量及其奇偶性不受影响。而多考虑这一位的原因是极端情况下,比如 1 × 1 1 \times 1 1×1的矩阵,只有一个数,如果不考虑最后一位 0 0 0,那么函数 m e r g e ( 1 , n × n − 1 ) = m e r g e ( 1 , 0 ) merge(1, n \times n - 1) = merge(1, 0) merge(1,n×n1)=merge(1,0),会造成内存越界。

当然如果将函数 m e r g e merge merge中的 i f if if判断条件修改成 ≥ \ge 的时候,是可以仅考虑 n × n − 1 n \times n - 1 n×n1个数的。代码见下面。

#include 
#include 
#include 
using namespace std;

typedef long long LL;
const int N = 510 * 510 + 10;

int n;
int q[N], tmp[N];
LL cnt1, cnt2; // 记录两个局面的逆序对数

LL merge(int l, int r) {
    if (l == r) return 0;
    int mid = (l + r) >> 1;
    LL res = merge(l, mid) + merge(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else {
            res += mid - i + 1;
            tmp[k++] = q[j++];
        }
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    for (int i = l, j = 0; i <= r; i++, j++)
        q[i] = tmp[j];
    return res;
}

int main() {
    while (cin >> n) {
        int ok = 0, x;
        for (int i = 1; i <= n * n; i++) {
            cin >> x;
            if (!x) ok = 1;
            else q[i - ok] = x;
        }
        cnt1 = merge(1, n * n);  // 这里有个细节,见题解注释

        ok = 0;
        for (int i = 1; i <= n * n; i++) {
            cin >> x;
            if (!x) ok = 1;
            else q[i - ok] = x;
        }
        memset(tmp, 0, sizeof tmp);
        cnt2 = merge(1, n * n);

        if ((cnt1 & 1) == (cnt2 & 1)) puts("TAK");
        else puts("NIE");

        memset(q, 0, sizeof q);
    }
    return 0;
}

仅考虑 n × n − 1 n \times n - 1 n×n1个数。

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 510 * 510;

int n;
int q[N], tmp[N];

LL get(int l, int r) {
    if (l >= r) return 0;
    int mid = l + r >> 1;
    LL res = get(l, mid) + get(mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else res += mid - i + 1, tmp[k++] = q[j++];
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];
    for (int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
    return res;
}

int main() {
    while (scanf("%d", &n) != EOF) {
        int ok = 0, x;
        for (int i = 0; i < n * n; i++) {
            scanf("%d", &x);
            if (!x) ok = 1; else q[i - ok] = x;
        }
        LL res1 = get(0, n * n - 1);
        ok = 0;
        for (int i = 0; i < n * n; i++) {
            scanf("%d", &x);
            if (!x) ok = 1; else q[i - ok] = x;
        }
        LL res2 = get(0, n * n - 1);
        if ((res1 & 1) == (res2 & 1)) puts("TAK"); else puts("NIE");
    }
    return 0;
}

3 二分查找算法

3.1 二分查找算法模板

  • 对第二段代码中如果也采用mid = (l + r) >> 1,那么当 r − l = 1 r - l = 1 rl=1 的时候,就会有mid = (l + r) >> 1 = l。假如接下来进入 l = m i d l = mid l=mid 分支,可行区间未缩小,造成死循环;若进入 r = m i d − 1 r = mid - 1 r=mid1 分支,造成 l > r l > r l>r,循环不能以 l = r l=r l=r 结束。
  • 对于两种 m i d mid mid 的取法进行分析,其中mid = (l + r) >> 1不会取到 r r r 这个值,mid = (l + r + 1) >> 1不会取到 l l l 这个值。这样可以利用这个性质来处理无解的情况,将最初的二分区间 [ 1 , n ] [1,n] [1,n] 分别扩大为 [ 1 , n + 1 ] [1, n +1] [1,n+1] [ 0 , n ] [0, n] [0,n],把数组 a a a 的一个越界下标包含进来。如果最后二分终止于扩大后的这个越界下标上,则说明 a a a 中不存在所求的数。
  • 在二分代码的模板中,使用了右移运算>>1而不是整数除法/2。这是因为右移运算是向下取整,整数除法是向零取整,在二分值域包含负数的时候后者不能正常工作。
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r) {
    while (l < r) {
        int mid = l + (r - l >> 1); // 防止加法越界
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}

// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r) {
    while (l < r) {
        int mid = l + (r - l + 1 >> 1); // 防止加法越界
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

3.2 数的范围

ACWing 789

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, m, q[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%d", q + i);
    while (m--) {
        int x; scanf("%d", &x);
        int l = 0, r = n - 1;
        while (l < r) { // 查找起始位置
            int mid = (l + r) >> 1;
            if (q[mid] >= x) r = mid;
            else l = mid + 1;
        }
        if (q[l] != x) {
            printf("-1 -1\n");
            continue;
        }
        printf("%d ", l);
        l = 0, r = n - 1;
        while (l < r) { // 查找终止位置
            int mid = (l + r + 1) >> 1;
            if (q[mid] <= x) l = mid;
            else r = mid - 1;
        }
        printf("%d\n", l);
    }
    return 0;
}

3.3 数的三次方根(浮点数的二分)

ACWing 790

算法思想

就是在-100~100中适用二分查找去寻找mid * mid * mid == x的数,且其精度按经验值要比题目要求的精度高两位。

#include 
#include 
using namespace std;

int main() {
    double x; cin >> x;
    double l = -100, r = 100;
    while (r - l > 1e-8) {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    printf("%.6lf\n", l);
    return 0;
}

3.4 倒垃圾

ACwing 4480

二分:手写二分 O ( n l o g n ) O(nlogn) O(nlogn)

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 2e5 + 10;

int n, m;
int c[N], a[N], b[N];
int ans[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m + n; i++) scanf("%d", c + i);
    for (int i = 1, j = 1, k = 1; i <= m + n; i++) {
        int t; scanf("%d", &t);
        if (t) b[j++] = c[i];
        else a[k++] = c[i];
    }
    b[0] = -2e9, b[m + 1] = 2e9;
    for (int i = 1; i <= n; i++) {
        int l = 0, r = m + 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (a[i] <= b[mid]) r = mid;
            else l = mid + 1;
        }
        if ((LL) b[r] - a[i] >= (LL) a[i] - b[r - 1]) ans[r - 1]++;
        else ans[r]++;
    }
    for (int i = 1; i <= m; i++) printf("%d ", ans[i]);
    puts("");
    return 0;
}

二分:库函数二分 O ( n l o g n ) O(nlogn) O(nlogn)

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 2e5 + 10;

int n, m;
int c[N], a[N], b[N];
int ans[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m + n; i ++ ) scanf("%d", c + i);
    for (int i = 1, j = 1, k = 1; i <= m + n; i ++ ) {
        int t; scanf("%d", &t);
        if (t) b[j ++ ] = c[i];
        else a[k ++ ] = c[i];
    }
    b[0] = -2e9, b[m + 1] = 2e9;
    for (int i = 1; i <= n; i ++ ) {
        int r = lower_bound(b, b + m + 2, a[i]) - b;
        if ((LL) b[r] - a[i] >= (LL) a[i] - b[r - 1]) ans[r - 1] ++ ;
        else ans[r] ++ ;
    }
    for (int i = 1; i <= m; i ++ ) printf("%d ", ans[i]);
    puts("");
    return 0;
}

双指针 O ( n ) O(n) O(n)

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 2e5 + 10;

int n, m;
int c[N], a[N], b[N];
int ans[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m + n; i ++ ) scanf("%d", c + i);
    for (int i = 1, j = 1, k = 1; i <= m + n; i ++ ) {
        int t; scanf("%d", &t);
        if (t) b[j ++ ] = c[i];
        else a[k ++ ] = c[i];
    }
    b[0] = -2e9, b[m + 1] = 2e9;
    for (int i = 1, j = 0; i <= n; i ++ ) {
        while (b[j] < a[i]) j ++ ;
        int r = j;
        if ((LL) b[r] - a[i] >= (LL) a[i] - b[r - 1]) ans[r - 1] ++ ;
        else ans[r] ++ ;
    }
    for (int i = 1; i <= m; i ++ ) printf("%d ", ans[i]);
    puts("");
    return 0;
}

3.5 最佳牛围栏(求一个平均数最大的、长度不小于 L L L 的连续子段)

ACwing 102

给定原始数组 a [   ] a[\space] a[ ],假设通过二分给定一个平均值 a v g avg avg,判断是否存在一个方案使得平均值 ≥ a v g \ge avg avg?如果将 a [   ] a[\space] a[ ]中的每一个值都执行 a [ i ] − = a v g a[i] -= avg a[i]=avg(如果原数大于平均值,那么其减去平均值大于零,否则小于零),并且预处理处一个前缀和 S i S_i Si。问题就等价于 a [   ] a[\space] a[ ]中是否存在一段长度大于等于 F F F a [ i ] − = a v g a[i] -= avg a[i]=avg,其和 ≥ 0 \ge 0 0,即是否存在一个方案使得平均值 ≥ a v g \ge avg avg

下面分析类似于DP:

这里的每一段,即区间,可以这样分类。按右端点所在在位置 i ∈ [ 1 , n ] i\in[1,n] i[1,n]来分类,分为 n n n类,只需要求每一类的最大值,最后求整体最大值即可。

假设右端点为 k k k,那么左端点 i ∈ [ 1 , k − F + 1 ] i \in [1,k-F+1] i[1,kF+1],对于每一种右端点 k k k,可以枚举其所有的左端点 i i i。那么这段区间的和为 S k − S i − 1 S_k - S_{i-1} SkSi1,其中 i − 1 ∈ [ 0 , k − F ] i-1 \in [0,k-F] i1[0,kF]。要使 S k − S i − 1 S_k - S_{i-1} SkSi1最大,只需要使 S i − 1 S_{i-1} Si1最小即可。因为这里的 S i − 1 S_{i-1} Si1是随着 k k k的增大而增大的,所以可以使用一个变量 m i n s mins mins来存储。

第一章 基础算法(1)_第1张图片

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, F;
double a[N], s[N];

bool check(double avg) {
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i] - avg;
    double mins = 0;
    for (int k = F; k <= n; k++) {
        mins = min(mins, s[k - F]);
        if (s[k] >= mins) return true;
    }
    return false;
}

int main() {
    scanf("%d%d", &n, &F);
    double l = 0, r = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%lf", a + i);
        r = max(r, a[i]);
    }
    while (r - l > 1e-5) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    // 这里输出只能使用r,不能用l,因为r和l可能有差值
    printf("%d\n", (int) (r * 1000));
    return 0;
}

3.6 特殊排序

ACwing 113

注意题目仅要求相邻两项 a i < a i + 1 a_i < a_{i+1} ai<ai+1,因为不具有传递性但有对称性,即 a 1 < a 2 , a 2 < a 3 a_1 < a_2,a_2 < a_3 a1<a2a2<a3,但是可能有 a 3 < a 1 a_3 < a_1 a3<a1

算法思路类似于插入排序。给定 N N N个数,将这 N N N个数排个序,即对于每个数都考虑应该插入到序列中的什么位置。

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int> res(1, 1); // 先插入1
        for (int i = 2; i <= N; i++) {
            int l = 0, r = res.size() - 1;
            while (l < r) {
                int mid = (l + r + 1) >> 1;
                if (compare(res[mid], i)) l = mid; // 找到第一个小于i的数
                else r = mid - 1;
            }
            res.push_back(i);
            // 不断交换位置,使最后一个数的位置变到正确的r+1的位置,将i插入到r + 1的位置
            for (int j = res.size() - 2; j > r; j--) swap(res[j], res[j + 1]);
            // 判断边界:i小于res中的所有数,此时r=0
            if (compare(i, res[r])) swap(res[r], res[r + 1]);
        }
        return res;
    }
};

3.7 找出第 K 小的数对距离

LeetCode 719

排序+二分

class Solution {
public:
    int smallestDistancePair(vector<int> &nums, int k) {
        sort(nums.begin(), nums.end());
        int n = nums.size(), left = 0, right = nums.back() - nums.front();
        while (left < right) {
            int mid = (left + right) >> 1;
            int cnt = 0;
            for (int j = 0; j < n; j++) {
                int i = lower_bound(nums.begin(), nums.begin() + j, nums[j] - mid) - nums.begin();
                cnt += j - i;
            }
            if (cnt >= k) right = mid;
            else left = mid + 1;
        }
        return left;
    }
};

排序+二分+双指针

class Solution {
public:
    int smallestDistancePair(vector<int> &nums, int k) {
        sort(nums.begin(), nums.end());
        int n = nums.size(), left = 0, right = nums.back() - nums.front();
        while (left < right) {
            int mid = (left + right) >> 1;
            int cnt = 0;
            for (int i = 0, j = 0; j < n; j++) {
                while (nums[j] - nums[i] > mid) i++;
                cnt += j - i;
            }
            if (cnt >= k) right = mid;
            else left = mid + 1;
        }
        return left;
    }
};

3.8 防线

ACwing 120

在数轴上,存在多个整数等差数列,每个等差数列数列上的、对应数轴上的每个位置都有一个数,记为 p p p,这样数轴上的每个整数点的值为所有等差数列在该位置上的 ∑ p \sum p p之和。题目保证 ∑ p \sum p p为奇数的位置最多只有一个,其余所有的 p p p都是偶数,如果无解即为所有的 ∑ p \sum p p都是偶数。题目要求找出奇数 p p p的位置。

算法思路:(前缀和 + 二分)

参考题解

1、奇数位存在性
整个序列中至多有一个位置的数字所占数量是奇数,所以如果存在奇数位,则整个数列的总和必然是奇数(奇数 + 偶数 = 奇数,偶数 + 偶数 = 偶数)。反之,若不存在奇数位,则一定是偶数。故只需判断数字数量的总和的奇偶性即可。

2、二分位置
若存在这个奇数位,就可以通过二分答案的位置来找到这个位置,然后判断区间 [ l ,   m i d ] [l,\ mid] [l, mid]总和的奇偶性。若为奇数,则奇数位存在于此区间。反之若为偶数,则一定存在于 [ m i d + 1 ,   r ] [mid+1,\ r] [mid+1, r]区间。用这个方法逐步缩小范围即可。对于求解区间 [ l ,   m i d ] [l,\ mid] [l, mid]之和,可以使用前缀和 s u m [ m i d ] − s u m [ l − 1 ] sum[mid] - sum[l - 1] sum[mid]sum[l1]即可。

3、在 O ( n ) O(n) O(n)时间内求出 s u m [ x ] sum[x] sum[x]

首先考虑对于对于一个等差数列(起点 S i S_i Si,终点为 E i E_i Ei,公差为 D i D_i Di),该等差数列中在 x x x左边的数有多少个:

  • 如果该等差数列 S i > x S_i > x Si>x,那么在 x x x左边的数有 0 0 0个;
  • 如果该等差数列 S i ≤ x S_i \le x Six,那么在 x x x左边的数为 中间间隔数目+ 1,即 m i n ( x , E i ) − S i D i + 1 \frac{min(x, E_i) - S_i}{D_i} + 1 Dimin(x,Ei)Si+1该时间复杂度为 O ( 1 ) O(1) O(1)

那么对于所有的等差数列,其在 x x x左边的数的个数为 ∑ i = 1 n ( m i n ( x , E i ) − S i D i + 1 ) \sum_{i = 1}^n (\frac{min(x, E_i) - S_i}{D_i} + 1) i=1n(Dimin(x,Ei)Si+1)该时间复杂度为 O ( n ) O(n) O(n)

时间复杂度

二分时间为 O ( l o g n ) O(logn) O(logn),每一次 c h e c k ( ) check() check()时间为 O ( n ) O(n) O(n),因此总共时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 200010;

int n;
struct Seq {
    int s, e, d;
} seq[N];

LL sum(int x) {
    LL res = 0;
    for (int i = 0; i < n; i++)
        if (seq[i].s <= x)
            res += (min(seq[i].e, x) - seq[i].s) / seq[i].d + 1;
    return res;
}

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        int l = 0, r = 0; scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf("%d%d%d", &seq[i].s, &seq[i].e, &seq[i].d);
            r = max(r, seq[i].e);
        }
        while (l < r) {
            int mid = ((LL) l + r) >> 1;
            if (sum(mid) & 1) r = mid;
            else l = mid + 1;
        }
        LL res = sum(r) - sum(r - 1);
        if (res & 1) printf("%d %lld\n", r, res);
        else puts("There's no weakness.");
    }
    return 0;
}

3.9 赶牛入圈

ACwing 121

题意:在平面上有多个点,每个点上有一个数值,现在在平面上找一个正方形,使得正方形中包含的数值之和至少为 C C C的前提下,正方形的边长最小。

算法思路:(前缀和 + 离散化 + 二分)

因为牛的横纵坐标 x 、 y ∈ [ 1 , 10000 ] x、y \in [1, 10000] xy[1,10000],如果直接开一个 10000 × 10000 10000 \times 10000 10000×10000的数组会非常大。

首先需要对每头牛的坐标进行离散化,这里将横纵坐标放在一起做的离散化。然后获取离散化坐标的二维前缀和 s u m [   ] [   ] sum[\ ][\ ] sum[ ][ ]。最后在 [ 1 ,   10000 ] [1, \ 10000] [1, 10000]中枚举正方形的边长,如果该正方形的前缀和大于等于 C C C,则缩小边长,否则扩大边长。

代码中 c h e c e ( ) chece() chece()函数两点解释:

  • 一个是 ( x 1 ,   y 1 ) 、 ( x 2   y 2 ) (x1,\ y1)、(x2\ y2) (x1, y1)(x2 y2)分别代表矩形左上角和右下角坐标,且是离散化之后的坐标;第一章 基础算法(1)_第2张图片
  • w h i l e ( ) while() while()中要 + 1 +1 +1的原因;第一章 基础算法(1)_第3张图片
  • 注意理解这里的离散化,离散化之后相当于是重新建立了一个新的坐标轴,其范围为 [ 1 ,   a l l s . s i z e ( ) ] [1,\ alls.size()] [1, alls.size()],然后在这里枚举所有的矩形,看是否存在前缀和大于等于 C C C的矩形。

对代码中 numbers.push_back(0); 的解释:倘若不加上这一句, 在函数 c h e c k check check 使用双指针计算前缀和的时候会使用到 x 1 = 0 x1 = 0 x1=0,这样就需要特殊处理。

#include 
#include 
#include 
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 1010;

int n, m;
PII points[N];
int sum[N][N];
vector<int> alls;

bool check(int len) {
    for (int x1 = 1, x2 = 1; x2 < alls.size(); x2++) {  
        while (alls[x2] + 1 - alls[x1] > len) x1++;
        for (int y1 = 1, y2 = 1; y2 < alls.size(); y2++) {
            while (alls[y2] + 1 - alls[y1] > len) y1++;
            if (sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1] >= m) return true;
        }
    }
    return false;
}

int get(int x) {
    int l = 0, r = alls.size() - 1;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}

int main() {
    cin >> m >> n;
    alls.push_back(0);
    for (int i = 0; i < n; i++) {
        int x, y; cin >> x >> y;
        alls.push_back(x), alls.push_back(y);
        points[i] = {x, y};
    }
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    for (int i = 0; i < n; i++) {
        int a = get(points[i].x), b = get(points[i].y);
        sum[a][b]++;
    }
    for (int i = 1; i < alls.size(); i++)  // 这里不能等于,因为多了一个0
        for (int j = 1; j < alls.size(); j++) 
        	sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
    int l = 1, r = 10000;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << r << endl;
    return 0;
}

4 高精度计算

4.1 高精度加法

ACWing 791

#include 
#include 
#include 
using namespace std;

vector<int> add(vector<int> &A, vector<int> &B) {
    vector<int> C;
    for (int i = 0, t = 0; i < A.size() || i < B.size() || t; i++) {
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }
    return C;
}

int main() {
    string a, b; cin >> a >> b;
    vector<int> A, B;
    for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
    auto C = add(A, B);
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    cout << endl;
    return 0;
}

4.2 高精度减法

ACWing 792

#include 
#include 
#include 
using namespace std;

bool cmp(vector<int> &A, vector<int> &B) {
    if (A.size() != B.size()) return A.size() > B.size();
    for (int i = A.size() - 1; i >= 0; i--)
        if (A[i] != B[i])
            return A[i] > B[i];
    return true;
}

vector<int> sub(vector<int> &A, vector<int> &B) {
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i++) {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }
    while (C.size() > 1 && !C.back()) C.pop_back();
    return C;
}

int main() {
    string a, b; cin >> a >> b;
    vector<int> A, B;
    for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
    vector<int> C;
    if (cmp(A, B)) C = sub(A, B);
    else C = sub(B, A), cout << '-';
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    cout << endl;
    return 0;
}

4.3 高精度乘法

ACWing 793

#include 
#include 
#include 
using namespace std;

vector<int> mul(vector<int> &A, int b) {
    vector<int> C;
    for (int i = 0, t = 0; i < A.size() || t; i++) { // 这里的t是用来处理最后算完后依然会有进位的情况!
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }
    while (C.size() > 1 && !C.back()) C.pop_back();
    return C;
}

int main() {
    string a; int b; cin >> a >> b;
    vector<int> A;
    for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    auto C = mul(A, b);
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    cout << endl;
    return 0;
}

4.4 高精度除法

ACWing 794

#include 
#include 
#include 
using namespace std;

vector<int> div(vector<int> &A, int b, int &r) {
    vector<int> C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i--) {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && !C.back()) C.pop_back();
    return C;
}

int main() {
    string a; int B; cin >> a >> B;
    vector<int> A;
    for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    int r;
    auto C = div(A, B, r);
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    cout << endl << r << endl;
    return 0;
}

4.5 数的进制转换

ACwing 124

记输入进制为 a a a进制,输出进制为 b b b进制。

一般做法:
先将 a a a进制转换为 10 10 10进制数,使用秦九韶算法,需要使用高精度,如 ( 12345 ) 5 → ( ( ( 1 × 5 + 2 ) × 5 + 3 ) × 5 + 4 ) × 5 + 5 ) 10 (12345)_5 \rightarrow (((1 \times 5 + 2) \times5 + 3) \times5 + 4)\times 5 +5)_{10} (12345)5(((1×5+2)×5+3)×5+4)×5+5)10然后将 10 10 10进制数转换为 b b b进制数,使用短除法,如 ( 20 ) 10 → ( 202 ) 3 (20)_{10} \rightarrow (202)_3 (20)10(202)3求解的方法是: x = 20 = 2 × 3 2 + 0 × 3 1 + 2 × 3 0 →   x   %   3 = 20   %   3 = 2 , x = x / 3 = 6 →   x   %   3 = 6   %   3 = 0 , x = x / 3 = 2 →   x   %   3 = 2   %   3 = 2 , x = x / 3 = 0 \begin{aligned} &x = 20 = {\color{blue}{2}} \times 3^2 + {\color{red}{0}} \times 3^1 + {\color{green}{2}} \times 3^0\\ \rightarrow\ &x\ \% \ 3 = 20\ \% \ 3 = {\color{green}{2}},x = x / 3 = 6\\ \rightarrow\ &x\ \% \ 3 = 6 \ \% \ 3 = {\color{red}{0}},x = x / 3 = 2\\ \rightarrow\ &x\ \% \ 3 = 2 \ \% \ 3 = {\color{blue}{2}},x = x / 3 = 0\\ \end{aligned}    x=20=2×32+0×31+2×30x % 3=20 % 3=2x=x/3=6x % 3=6 % 3=0x=x/3=2x % 3=2 % 3=2x=x/3=0

优化做法:
核心思想依然是短除法。如图:第一章 基础算法(1)_第4张图片

#include 
#include 
#include 
using namespace std;

int main() {
    int T; cin >> T;
    while (T -- ) {
        int a, b; string line;
        cin >> a >> b >> line;
        vector<int> number;
        for (auto c : line)
            if (c <= '9') number.push_back(c - '0');
            else if (c <= 'Z') number.push_back(c - 'A' + 10);
            else number.push_back(c - 'a' + 36);
        reverse(number.begin(), number.end());

        vector<int> res;
        while (number.size()) {
            int t = 0;
            for (int i = number.size() - 1; i >= 0; i -- ) {
                number[i] += t * a;
                t = number[i] % b;
                number[i] /= b;
            }
            res.push_back(t);
            while (number.size() && !number.back()) number.pop_back();
        }
        reverse(res.begin(), res.end());

        string b_line;
        for (auto x : res)
            if (x <= 9) b_line += char('0' + x);
            else if (x <= 35) b_line += char('A' + x - 10);
            else b_line += char('a' + x - 36);

        cout << a << ' ' << line << endl;
        cout << b << ' ' << b_line << endl;
        cout << endl;
    }
    return 0;
}

5 前缀和与差分

5.1 前缀和

ACWing 795

#include 
using namespace std;

const int N = 1e5 + 10;

int n, m, s[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int x; scanf("%d", &x);
        s[i] += s[i - 1] + x;
    }
    while (m--) {
        int l, r; scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);
    }
    return 0;
}

5.2 子矩阵的和——二维前缀和

ACWing 796

两个公式:

  • s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
  • x = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]
#include 
using namespace std;

const int N = 1010;

int n, m, q;
int s[N][N];

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            int x; scanf("%d", &x);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;
        }
    while (q--) {
        int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }
    return 0;
}

5.3 一维差分数组

ACWing 797

Tips: 对差分数组求前缀和就是对应的前缀和数组,差分数组便于修改!本题的关键就在于,要在L、R之间的元素添加c,就是在原数组的差分数组里的L位置上的值加上c,在R+1的位置上减去c,这样求前缀和的时候,L及其之后的元素都会加上c,R+1及其之后的元素都会减去c,这样的前缀和数组即为所求。

#include 
using namespace std;

const int N = 1e5 + 10;

int n, m, b[N];

void insert(int l, int r, int c) {
    b[l] += c;
    b[r + 1] -= c;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int x; scanf("%d", &x);
        insert(i, i, x);
    }
    while (m--) {
        int l, r, c; scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    for (int i = 1; i <= n; i++) {
        b[i] += b[i - 1];
        printf("%d ", b[i]);
    }
    return 0;
}

5.4 求差分矩阵——二维差分

ACWing 798

注意:差分数组的构造

void insert(int x1, int y1, int x2, int y2, int c) {
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}
#include 
using namespace std;

const int N = 1010;

int n, m, q, x, b[N][N];

void insert(int x1, int y1, int x2, int y2, int c) {
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            scanf("%d", &x);
            insert(i, j, i, j, x);
        }
    while (q--) {
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        insert(x1, y1, x2, y2, c);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
            printf("%d ", b[i][j]);
        }
        printf("\n");
    }
    return 0;
} 

5.5 激光炸弹

ACwing 99

因为题目要求“可以摧毁一个包含 R × R R \times R R×R 个位置的正方形内的所有目标”,因此对于 R × R R \times R R×R的矩阵,边上的点不能计算,故最多只能包含 ( R − 1 ) × ( R − 1 ) (R-1) \times (R-1) (R1)×(R1)个点,所以可以使用二维前缀和,然后枚举一个 ( R − 1 ) × ( R − 1 ) (R-1) \times (R-1) (R1)×(R1)矩阵里面所能包含格子的最大值即可。

注意:

  1. 题目中的目标是一个点,且这些点不能处于矩形 R × R R \times R R×R的边上;
  2. m a x ( R ) = 5001 max(R) = 5001 max(R)=5001 是因为坐标都 + 1 +1 +1了。
#include 
#include 
using namespace std;

const int N = 5010;

int s[N][N];

int main() {
    int n, R; scanf("%d%d", &n, &R);
    R = min(R, 5001); // R最大值只能取到5001
    for (int i = 0; i < n; i++) {
        int x, y, w; scanf("%d%d%d", &x, &y, &w);
        x++, y++; s[x][y] += w; // 将所有的下标从1开始
    }
    for (int i = 1; i <= 5001; i++)
        for (int j = 1; j <= 5001; j++)
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
    int res = 0;
    for (int i = R; i <= 5001; i++) // 求(R-1)*(R-1)矩阵和
        for (int j = R; j <= 5001; j++)
            res = max(res, s[i][j] - s[i - R][j] - s[i][j - R] + s[i - R][j - R]);
    printf("%d\n", res);
    return 0;
}

5.6 增减序列

ACwing 100

假设原始序列为 a 1 、 a 2 、 . . . 、 a n a_1、a_2、...、a_n a1a2...an,那么差分序列为 b 1 = a 1 b 2 = a 2 − a 1 . . . b n = a n − a n − 1 \begin{aligned} &b_1 = a_1\\ &b_2 = a_2 - a_1\\ &...\\ &b_n = a_n - a_{n-1}\\ \end{aligned} b1=a1b2=a2a1...bn=anan1要使所有的数值一样,就等价于 b 1 b_1 b1可以任取,但是 b 2 = b 3 = . . . b n = 0 b_2 = b_3 = ... b_n = 0 b2=b3=...bn=0

原问题就转换为了

  1. 至少操作多少次,可以使 b 2 = b 3 = . . . = b n = 0 b_2 = b_3 = ... = b_n = 0 b2=b3=...=bn=0
  2. 在至少操作次数前提下, b 1 b_1 b1有多少种值?

我们将对差分数组的操作分为四大类:

  1. 2 ≤ L ≤ R ≤ n − 1 2 \le L \le R \le n-1 2LRn1,在 [ b 2 , b n ] [b_2,b_n] [b2,bn]中,令某一个数+1,另一个数-1,正负匹配,这种方法最快,应该优先选择;
  2. L = 1 , R ≤ n − 1 L= 1,R \le n-1 L=1Rn1, 令 b 1 b_1 b1+1 or -1 [ b 2 , b n ] [b_2, b_n] [b2,bn]中的某个数+1 or -1
  3. 2 ≤ L , R = n 2 \le L,R = n 2LR=n,令 b n + 1 b_{n+1} bn+1+1 or -1 [ b 2 , b n ] [b_2, b_n] [b2,bn]中的某个数+1 or -1
  4. L = 1 , R = n L = 1,R = n L=1R=n,令所有数都+1 or -1,无意义。

那么最少可以使用操作1、2、3多少次,可以使 b 2 = b 3 = . . . = b n = 0 b_2 = b_3 = ... = b_n = 0 b2=b3=...=bn=0呢?假设差分数组从 b 2 . . . b n b_2...b_n b2...bn中,所有正数之和为 p p p,所有负数之和为 q q q。那么可以优先使用操作1,进行正负匹配,操作min(p,q)次后,剩余|p-q|个数,使用操作2 or 3来完成即可,所以第一个问题即为min(p,q) + |p-q| = max(p,q)

剩余的|p-q|个数,可以这样分配:操作20个,操作3|p-q|个;操作21个,操作3|p-q|-1个;操作22个,操作3|p-q|-2个;…;操作2|p-q|个,操作30个。那么只有操作2会改变 b 1 b_1 b1的值,所以第二问的答案为|p-q|+1

#include 
#include 
using namespace std;

typedef long long LL;
const int N = 1e5 + 10;

int n, s[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", s + i);
    for (int i = n; i >= 2; i--) s[i] -= s[i - 1];
    LL p = 0, q = 0;
    for (int i = 2; i <= n; i++)
        if (s[i] > 0) p += s[i]; else q -= s[i];
    printf("%lld\n%lld\n", max(p, q), abs(p - q) + 1);
    return 0;
}

5.7 最高的牛

ACwing 101

使用一个数组 C C C C [ i ] C[i] C[i]记录第 i i i头牛的身高比最高的那头牛矮多少,初始化全部为 0 0 0。假如存在一组关系有 A i 、 B i A_i、B_i AiBi A i < B i A_i < B_i Ai<Bi,那么我们将数组 C C C中下标 A i + 1 ∼ B i − 1 A_i + 1 \sim B_i - 1 Ai+1Bi1上的值全部减去 1 1 1,即表示在 A i 、 B i A_i、B_i AiBi中间的牛,身高至少比它们小 1 1 1。因为第 P P P头牛是最高的牛,那么最后一定满足 C [ P ] = 0 C[P] = 0 C[P]=0。那么最后,第 i i i头牛的身高为 H + C [ i ] H + C[i] H+C[i]

如果朴素执行把数组 C C C中下标为 A i + 1 ∼ B i − 1 A_i + 1 \sim B_i - 1 Ai+1Bi1的数都减去 1 1 1的操作,那么整个算法的时间复杂度为 O ( N M ) O(NM) O(NM)。优化办法为新建一个差分数组 D D D,每次对 A i 、 B i A_i、B_i AiBi操作的时候,令 D [ A i + 1 ] − 1 D[A_i + 1] - 1 D[Ai+1]1 D [ B i ] + 1 D[B_i]+1 D[Bi]+1

优化后的算法那对一个区间的操作转化为左右两个端点上的操作,再通过前缀和得到原问题的解,故该算法时间复杂度为: O ( N + M ) O(N+M) O(N+M)

#include 
#include 
#include 
using namespace std;

const int N = 5010;

int n, p, h, m;
int s[N];
set<pair<int, int>> st;

int main() {
    cin >> n >> p >> h >> m;
    while (m--) {
        int a, b; cin >> a >> b;
        if (a > b) swap(a, b);
        if (st.count({a, b})) continue;
        st.insert({a, b});
        s[a + 1]--, s[b]++;
    }
    for (int i = 1; i <= n; i++) {
        s[i] += s[i - 1];
        cout << s[i] + h << endl;
    }
    return 0;
}

5.8 最长平衡串

ACwing 3553

如果一个子串中 0 0 0 1 1 1的数量相同,就称它为平衡子串。容易想到前缀和,记原数组为 c [ n ] c[n] c[n],使用数组 a [ i ] 、 b [ i ] a[i]、b[i] a[i]b[i]分别记录原数组中前 i i i个元素中 0 0 0 1 1 1的个数。

对于原数组的任意一段字串 c [ j ] , c [ j + 1 ] , ⋯   , c [ i ] c[j],c[j + 1], \cdots, c[i] c[j],c[j+1],,c[i]上,假设在区间 [ j + 1 , i ] [j + 1, i] [j+1,i]上所有 0 、 1 0、1 01的个数相等,即满足 a [ i ] − a [ j ] = b [ i ] − b [ j ] a[i] - a[j] = b[i] - b[j] a[i]a[j]=b[i]b[j],即有 a [ i ] − b [ i ] = a [ j ] − b [ j ] a[i] - b[i] = a[j] - b[j] a[i]b[i]=a[j]b[j]令数组 s [ i ] = a [ i ] − b [ i ] s[i] = a[i] - b[i] s[i]=a[i]b[i],即表示原数组前 i i i个数中 0 0 0的个数比 1 1 1的个数多多少。如果区间 [ j , i ] [j, i] [j,i]是一个平衡子串,就等价于 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j]

所以可以枚举以 i i i为右端点的子串中找到一个最长的平衡子串,即找到一个最小的 j j j使得 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j]。这里可以使用一个哈希表存储 s [ i ] s[i] s[i]最早出现的位置。

#include 
#include 
#include 
#include 
using namespace std;

const int N = 1e6 + 10;

int n;
char str[N];

int main() {
    scanf("%s", str + 1);
    unordered_map<int, int> hash;
    n = strlen(str + 1);
    int res = 0;
    hash[0] = 0;
    for (int i = 1, s = 0; i <= n; i++) {
        if (str[i] == '0') s++; // s表示0的个数比1的个数多多少,每次只会使用到s[i]
        else s--;
        if (hash.count(s)) res = max(res, i - hash[s]);
        else hash[s] = i;
    }
    printf("%d\n", res);
    return 0;
}

6 双指针算法

Tips:双指针算法一般有两种情况。

  • 对于一个序列,用两个指针维护一段区间。如6.1。
  • 对于两个序列,维护某种次序,比如2.1中合并两个有序序列以及6.2、6.3。

6.1 最长连续不重复子序列

ACWing 799

注意理解思路,参考题解,基于DP。

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n;
int q[N], s[N]; // q存储元素,s记录元素q[i]出现次数

int main() {
    scanf("%d", &n);
    int res = 0;
    for (int i = 0, j = 0; i < n; i++) {
        scanf("%d", q + i);
        s[q[i]]++;
        while (s[q[i]] > 1) s[q[j++]]--;
        res = max(res, i - j + 1);
    }
    printf("%d\n", res);
    return 0;
}

6.2 数组元素的目标和

ACWing 800

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, m, x;
int a[N], b[N];

int main() {
    scanf("%d%d%d", &n, &m, &x);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    for (int i = 0; i < m; i++) scanf("%d", b + i);
    for (int i = 0, j = m - 1; i < n; i++) {
        while (j >= 0 && a[i] + b[j] > x) j--;
        if (j >= 0 && a[i] + b[j] == x) printf("%d %d\n", i, j);
    }
    return 0;
}

6.3 判断子序列

ACWing 2816

这个题也可以使用DP,当有大量子串需要判断的时候,DP更适合!
见LeetCode题解

#include 
#include 
using namespace std;

const int N = 1e5 + 10;

int n, m;
int a[N], b[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    for (int i = 0; i < m; i++) scanf("%d", b + i);
    int i = 0, j = 0;
    while (i < n && j < m) {
        if (a[i] == b[j]) i++;
        j++;
    }
    if (i == n) puts("Yes");
    else puts("No");
    return 0;
}

6.4 统计得分小于 K 的子数组数目

LeetCode 2302

class Solution {
public:
    long long countSubarrays(vector<int> &nums, long long k) {
        int n = nums.size();
        vector<long long> sum(n + 1);
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        long long res = 0;
        for (int i = 1, j = 1; i <= n; i++) {
            while ((i - j + 1) * (sum[i] - sum[j - 1]) >= k) j++;
            res += i - j + 1;
        }
        return res;
    }
};

6.5 盛最多水的容器

LeetCode 11

算法思路

容纳的水量 = 两个指针中的较小值 ∗ 指针之间的距离 容纳的水量 = 两个指针中的较小值 * 指针之间的距离 容纳的水量=两个指针中的较小值指针之间的距离

初始时,左右指针分别指向数组的左右两端,后面每一次都移动对应数字较小的指针。原因是如果移动数字较大的指针,前者「两个指针中的较小值」不会增加,后者「指针之间的距离」会减小,因此乘积会减小。按照这样的方式移动,移动过程中维护一个乘积最大值,直到两个指针重合结束。

算法正确性证明:
假设当前左指针和右指针指向的数分别为 x x x y y y,不妨设 x ≤ y x \le y xy。同时两个指针之间的距离为 t t t,那么这时容器容纳的水量为 m i n ( x , y ) × t = x × t min(x,y) \times t = x \times t min(x,y)×t=x×t假设现在向左移动右指针,其指向的数为 y 1 y_1 y1,两个指针之间的距离为 t 1 t_1 t1,显然有 t 1 < t t_1 < t t1<t,并且 m i n ( x , y 1 ) ≤ m i n ( x , y ) min(x, y_1) \le min(x, y) min(x,y1)min(x,y)

  • 如果 y 1 ≤ y y_1 \le y y1y,那么必然有 m i n ( x , y 1 ) ≤ m i n ( x , y ) min(x,y_1) \le min(x, y) min(x,y1)min(x,y)
  • 如果 y 1 > y y_1 > y y1>y,那么 m i n ( x , y 1 ) = x = m i n ( x , y ) min(x, y_1) = x = min(x, y) min(x,y1)=x=min(x,y)

因此有 m i n ( x , y 1 ) × t 1 < m i n ( x , y ) × t min(x ,y_1) \times t_1 < min(x, y) \times t min(x,y1)×t1<min(x,y)×t由此可知无论如何移动右指针,最终结果都会变得更小,与题目要求不符。

class Solution {
public:
    int maxArea(vector<int> &height) {
        int i = 0, j = height.size() - 1, area = 0;
        while (i < j) {
            area = max(area, min(height[i], height[j]) * (j - i));
            if (height[i] < height[j]) i++;
            else j--;
        }
        return area;
    }
};

6.6 颜色分类 (三数排序)

LeetCode 75

使用三个指针:

  • 0 ∼ j − 1 0 \sim j -1 0j1都是 0 0 0
  • j ∼ i − 1 j \sim i - 1 ji1都是 1 1 1
  • k ∼ n k \sim n kn都是2;

初始 i = j = 0 , k = n − 1 i = j = 0, k = n - 1 i=j=0,k=n1,当 i > k i > k i>k时,结束。

  • n u m s [ i ] = = 0 nums[i] == 0 nums[i]==0时, s w a p ( n u m s [ i ] , n u m s [ j ] ) ,   i + + , j + + swap(nums[i], nums[j]), \ i ++ , j ++ swap(nums[i],nums[j]), i++,j++
  • n u m s [ i ] = = 2 nums[i] == 2 nums[i]==2时, s w a p ( n u m s [ i ] , n u m s [ k ] ) ,   k − − swap(nums[i], nums[k]), \ k-- swap(nums[i],nums[k]), k,这时候 i i i就不能加,因为交换过来的 n u m s [ i ] nums[i] nums[i]不一定是 1 1 1
  • n u m s [ i ] = = 1 nums[i] == 1 nums[i]==1时,直接 i + + i ++ i++即可。
class Solution {
public:
    void sortColors(vector<int> &nums) {
        for (int i = 0, j = 0, k = nums.size() - 1; i <= k;) {
            if (!nums[i]) swap(nums[i++], nums[j++]);
            else if (nums[i] == 2) swap(nums[i], nums[k--]);
            else i++;
        }
    }
};

6.7 最小面积子矩阵

ACwing 3487

直接暴力枚举,左上角坐标和右下表坐标均为有 N 2 N^2 N2个,总共时间复杂度约为 O ( N 4 ) O(N^4) O(N4),可以过。但是这个题可以优化为一维。

因为矩阵中的所有值均非负数,当确定了矩形的上下边界,对于矩形的左右边界,记为 j 、 i j、i ji。若 j j j不变,随着 i i i的增大,矩形内部元素总和、元素个数(面积)都会增大。所以存在单调性,因此可以使用双指针进行优化。

可以重新定义一下 j j j:表示当 i i i固定的时候,最靠右且矩形中元素总和大于等于 k k k的、矩形的左边界坐标。

第一章 基础算法(1)_第5张图片

时间复杂度:上边界 O ( N ) O(N) O(N),下边界 O ( N ) O(N) O(N),中间双指针 O ( N ) O(N) O(N),因此总共时间复杂度为 O ( N 3 ) O(N^3) O(N3)

#include 
#include 
using namespace std;

const int N = 110, INF = 0x3f3f3f3f;

int n, m, k;
int s[N][N];

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            scanf("%d", &s[i][j]);
            s[i][j] += s[i - 1][j];
        }
    int res = INF;
    for (int x = 1; x <= n; x++)
        for (int y = x; y <= n; y++)
            for (int i = 1, j = 1, sum = 0; i <= m; i++) {
                sum += s[y][i] - s[x - 1][i];
                while (sum - (s[y][j] - s[x - 1][j]) >= k) 
                    sum -= s[y][j] - s[x - 1][j], j++;
                if (sum >= k) res = min(res, (y - x + 1) * (i - j + 1));
            }
    if (res == INF) res = -1;
    printf("%d\n", res);
    return 0;
}

6.8 最大的和

ACwing 126

二维前缀和问题

暴力做法

直接枚举每一个子矩阵的左上角坐标和右下角坐标,然后求一个最大值。时间复杂度 O ( n 4 ) O(n^4) O(n4)

优化做法

先考虑一维情况,假设存在组数据 a 1 , a 2 , ⋯   , a n a_1,a_2,\cdots,a_n a1,a2,,an要求出和最大的连续子序列

可以使用DP:设状态方程为 f ( i ) f(i) f(i),表示所有第 i i i个数结尾的、和最大的连续子序列的和。以倒数第二个数的值来划分,即 f ( i ) = { f ( i − 1 ) + a [ i ]   f ( i − 1 ) > 0 a [ i ] f ( i − 1 ) ≤ 0 f(i) = \begin{cases} f(i - 1) + a[i]\ &f(i - 1) > 0\\ a[i] &f(i - 1) \le 0\\ \end{cases} f(i)={f(i1)+a[i] a[i]f(i1)>0f(i1)0

一维空间中的最大子段和问题,题目ACwing 55 连续子数组的最大和

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN, last = 0;
        for (auto x : nums) {
            last = max(last, 0) + x;
            res = max(res, last);
        }
        return res;
    }
};

那么后面可以这样考虑二维问题:

  1. 对于二维矩阵,可以预先处理出每一列的前缀和;
  2. 枚举子矩阵横坐标的上下界(如图中绿线)
  3. 将上下界里面的矩阵的每一列通过前缀和快速求出每一列元素之和,然后将上下界的每一列元素之和看做该列的一个数,这样就将二维最大和转换为了一维最大和问题。

时间复杂度: O ( n 3 ) O(n^3) O(n3)

一维空间中的最大子段和问题,题目ACwing 55 连续子数组的最大和

#include 
#include 
#include 
using namespace std;

const int N = 110;

int n;
int g[N][N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            scanf("%d", &g[i][j]);
            g[i][j] += g[i - 1][j];
        }
    int res = INT_MIN;
    for (int i = 1; i <= n; i++)
        for (int j = i; j <= n; j++) {
            int last = 0;
            for (int k = 1; k <= n; k++) {
                last = max(last, 0) + g[j][k] - g[i - 1][k];
                res = max(res, last);
            }
        }
    printf("%d\n", res);
    return 0;
}

7 位运算

Tips:一些二进制中常用的操作

  1. 对于数值C,它的二进制表示为S,那么有~C = -1 - S
  2. (n >> k) & 1:取出整数n在二进制表示下的第k位;
  3. n & ((1 << k) - 1):取出整数n在二进制表示下的第0 ~ k-1位;
  4. n ^ (1 << k):把整数n在二进制下表示的第k位取反;
  5. n | (1 << k):把整数n在二进制下表示的第k位赋值为1
  6. n & (~(1 << k)):把整数n在二进制下的第k位赋值为0
  7. n & -n 返回n的最后一位 1
  8. x &= (x-1) 操作可以将 x 最右边的 1 删掉;
  9. 对于非负整数n
    • n为偶数时,n ^ 1 = n + 1
    • n为奇数时,n ^ 1 = n - 1
  10. 判断奇数偶数:x & 1 != 0,x为奇数;否则为偶数;
  11. l o w b i t lowbit lowbit:获取 n n n在二进制下最低位的 1 1 1及其后边的所有 0 0 0构成的数值,即lowbit(n) = n & (~n + 1) = n & (-n),因为补码下~n = -1 - n

Tips:C++ 高效位运算函数__builtin__

  • int __builtin_ffs(unsigned int x):返回x的最后一位1的是从后向前第几位;
  • int __builtin_clz (unsigned int x):返回前导的0的个数;
  • int __builtin_ctz (unsigned int x):返回后面的0个个数,和__builtin_clz相对;
  • int __builtin_popcount (unsigned int x):返回二进制表示中1的个数;
  • int __builtin_parity (unsigned int x):返回x的奇偶校验位,也就是x的1的个数模2的结果;

此外,这些函数都有相应的usigned longusigned long long版本,只需要在函数名后面加上l或ll就可以了,比如int __builtin_clzll

Tips:异或运算 X O R XOR XOR的一些性质

  • 交换律: a   X O R   b = b   X O R   a a \ XOR\ b = b \ XOR \ a a XOR b=b XOR a
  • 结合律: ( a   X O R   b )   X O R   c = a   X O R   ( b   X O R   c ) (a \ XOR \ b) \ XOR \ c = a \ XOR \ (b \ XOR \ c) (a XOR b) XOR c=a XOR (b XOR c)
  • 运算性质: 1   X O R   1 = 0 , 0   X O R   1 = 1 , a   X O R   a = 0 1 \ XOR \ 1 = 0, 0 \ XOR \ 1 = 1,a \ XOR \ a = 0 1 XOR 1=0,0 XOR 1=1,a XOR a=0

7.1 求二进制中1的个数

ACWing 801

// 解法一
int judge(int x) {
    int ret = 0;
    while (x) {
        x &= (x - 1); // 去掉x最右边的一个 1
        ret++;
    }
    return ret;
}

// 解法二
int judge(int x) {
    int ret = 0;
    for (int i = x; i; i -= i & -i) ret++; // 每一次循环减去 n 的最后一位 1
    return ret;
}

7.2 位操作练习

ACwing 3435

#include 
#include 
using namespace std;

int main() {
    int a, b;
    while (cin >> a >> b) {
        string x, y;
        for (int i = 15; i >= 0; i--) {
            x += to_string(a >> i & 1);
            y += to_string(b >> i & 1);
        }
        y += y;
        if (y.find(x) != -1) puts("YES");
        else puts("NO");
    }
    return 0;
}

7.3 二进制数

ACwing 3530

#include 
#include 
using namespace std;

int main() {
    unsigned int n;
    while (cin >> n) {
        string res;
        if (!n) res = "0";
        while (n) res += to_string(n & 1), n >>= 1;
        reverse(res.begin(), res.end());
        cout << res << endl;
    }
    return 0;
}

7.4 a^b

ACwing 89

#include 
#include 
using namespace std;

typedef long long LL;

int a, b, p;

LL qmi(int a, int b) {
    LL res = 1 % p;
    a %= p;
    while (b) {
        if (b & 1) res = res * a % p;
        a = (LL) a * a % p;
        b >>= 1;
    }
    return res;
}

int main() {
    scanf("%d%d%d", &a, &b, &p);
    printf("%lld\n", qmi(a, b));
    return 0;
}

7.5 64位整数乘法

ACwing 90

因为 a , b ≤ 1 0 18 a,b \le 10^{18} a,b1018,如果直接相乘,会爆long longlong long最大范围为 2 63 − 1 ≈ 9 × 1 0 18 2^{63}-1 \approx 9 \times 10^{18} 26319×1018)因此不能直接计算。方法可以使用高精度,或者快速加(基于快速幂的思想)。

  • 快速幂:乘法实现乘方
  • 快速加:加法实现乘法

为了避免爆long long,可以将 a × b → ( a + a + . . + a ) b 个 a 相加 a \times b → (a + a + .. + a)_{b个a相加} a×b(a+a+..+a)ba相加,所以两个数相加最大为 2 × 1 0 18 < 9 × 1 0 18 2 \times 10^{18} < 9 \times 10^{18} 2×1018<9×1018,是不会爆long long的。

朴素做法是每一次加一个 a a a,但是时间复杂度为 O ( b ) O(b) O(b)。如果 b = 1 0 18 b = 10^{18} b=1018,朴素做法就会超时。

优化方法基于快速幂的思想,分别求出 a   m o d   p 2 a   m o d   p 4 a   m o d   p 8 a   m o d   p . . . 2 64 a   m o d   p \begin{aligned} &a \space mod \space p\\ &2a \space mod \space p\\ &4a \space mod \space p\\ &8a \space mod \space p\\ &...\\ &2^{64}a \space mod \space p\\ \end{aligned} a mod p2a mod p4a mod p8a mod p...264a mod p每一个式子等于上一个式子的 2 2 2倍,又因为每一项都有 m o d   1 0 18 mod \space 10^{18} mod 1018,所以每一个式子的结果都在 1 0 18 10^{18} 1018以内。

拥有上述式子结果之后,假设 b = ( 11010 ) 2 = 2 4 + 2 3 + 2 1 b = (11010)_2 = 2^4 + 2^3 + 2^1 b=(11010)2=24+23+21,那么 a × b = 2 4 a + 2 3 a + 2 1 a a \times b = 2^4a + 2^3a + 2^1a a×b=24a+23a+21a中所有的值就是已知,因此 a × b a \times b a×b的结果就可以直接加起来即可,每次相加是不会爆long long的。

又因为 b b b的二进制表示最多有 l o g 2 b log_2^b log2b位,所以最多加 l o g 2 b log_2^b log2b次,因此时间复杂度为 l o g 2 b log_2^b log2b

#include 

typedef long long LL;

LL qadd(LL a, LL b, LL p) {
    LL res = 0;
    a %= p;
    while (b) {
        if (b & 1) res = (res + a) % p;
        a = (a + a) % p;
        b >>= 1;
    }
    return res;
}

int main() {
    LL a, b, p; scanf("%lld%lld%lld", &a, &b, &p);
    printf("%lld\n", qadd(a, b, p));
    return 0;
}

7.6 最短Hamilton路径

ACwing 91

思路见:第五章 动态规划(5):状态压缩模型 2.1

#include 
#include 
#include 
using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N]; // 两点之间的距离
int f[M][N];

int main() {
    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> w[i][j];
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0; // 从0走到0,里面所包含的元素只有0一个元素的路径
    for (int i = 0; i < 1 << n; i++) // 求每个状态
        for (int j = 0; j < n; j++) // j表示走到了哪一个点
            if (i >> j & 1) //	如果第j位上位1,则第j个数被选择
                for (int k = 0; k < n; k++) //k表示走到j这个点之前,以k为终点的最短距离
                    if (i >> k & 1)
                        f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + w[k][j]);
    cout << f[(1 << n) - 1][n - 1] << endl;
    return 0;
}

7.7 起床困难综合症

ACwing 998

原问题简述为:在 [ 1 , m ] [1, m] [1,m]找出一个数 x x x,求使得它经过 n n n次位运算之后所得的结果值最大。

算法思路:

对于 x x x的数值,从高位到地位枚举每一位,依次考虑每一位是该填 0 0 0还是填 1 1 1

对于 x x x的第 k k k位应该填 1 1 1,当且仅当同时满足一下两个条件:

  1. 已经填好的更高位构成的数值加上 1 << k 后不超过 m m m
  2. 用每个参数的第 k k k位参与运算,即calc函数计算。若初值为 1 1 1,则 n n n次位运算之后结果为 1 1 1;若初值为 0 0 0,则 n n n次位运算结果为 0 0 0

如果不满足上述条件,要么填 1 1 1会超过 m m m的范围,要么填 1 1 1不如填 0 0 0更优。这样令 x x x的第 k k k位为 0 0 0更好。

对第二点解释:为了使结果中出现尽可能多的 1 1 1,对于第二点中描述的情况,肯定应该填 1 1 1。对于其它情况:

  • 如果初始值为 0 0 0,结果为 1 1 1,这显然应该填 0 0 0
  • 如果初始值为 1 1 1,结果为 0 0 0,这样会使结果变小,故应该填 0 0 0

综上:记填 0 0 0后经过 n n n次位运算之后结果为 r e s 0 res0 res0,记填 1 1 1后经过 n n n次位运算之后结果为 r e s 1 res1 res1。如果 r e s 0 < r e s 1 res0 < res1 res0<res1,就填 1 1 1,否则填 0 0 0

代码中的一处解释:

  • 代码 m a i n main main中的第二个 f o r for for循环从 29 29 29开始循环:因为本题中 m m m最大是 1 0 9 10^9 109 l o g 2 1 0 9 = 3 l o g 2 1 0 3 < 3 × 10 = 30 log_2^{10 ^ 9} = 3log_2{10 ^ 3} < 3 \times 10 = 30 log2109=3log2103<3×10=30,所以每次 i i i 29 29 29往前枚举就可以了。
#include 
#include 
using namespace std;

#define x first
#define y second

typedef pair<string, int> PSI;
const int N = 1e5 + 10;

int n, m;
PSI op[N];

int work(int bit, int now) {
    for (int i = 1; i <= n; i ++ ) {
        int x = op[i].y >> bit & 1;
        if (op[i].x == "AND") now &= x;
        else if (op[i].x == "OR") now |= x;
        else now ^= x;
    }
    return now;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) {
        char str[5]; int t; scanf("%s%d", str, &t);
        op[i] = {str, t};
    }
    int val = 0, ans = 0;
    for (int bit = 29; bit >= 0; bit -- ) {
        int res0 = work(bit, 0), res1 = work(bit, 1);
        if (val + (1 << bit) <= m && res0 < res1)
            val += (1 << bit), ans += (res1 << bit);
        else ans += (res0 << bit);
    }
    printf("%d\n", ans);
    return 0;
}

7.8 只出现一次的数字 I

LeetCode 136

除了某个元素只出现一次以外,其余每个元素均出现两次

由异或运算性质 a   X O R   a = 0 , a   X O R   0 = a a \ XOR \ a = 0, a \ XOR \ 0 = a a XOR a=0,a XOR 0=a以及结合律,如果将两两相同的数结合起来,那么就会有 a   X O R   a = 0 a \ XOR \ a = 0 a XOR a=0,唯一一个不相同的数 0   X O R   b = b 0 \ XOR \ b = b 0 XOR b=b,所以直接将所有数进行异或运算即可。

class Solution {
public:
    int singleNumber(vector<int> &nums) {
        int res = 0;
        for (auto c: nums) res ^= c;
        return res;
    }
};

7.9 只出现一次的数字 II

LeetCode 137

除某个元素仅出现 一次 外,其余每个元素都恰出现三次

记「只出现了一次的元素」为「答案」。

方法1:依次确定每一个二进制位

由于数组中的元素都在 i n t int int范围内,因此可以依次计算答案的每一个二进制位是 0 0 0还是 1 1 1

具体地,考虑答案的第 i i i个二进制位是 0 0 0还是 1 1 1。因为对于数组中的非答案元素都出现了三次,对应着第 i i i个二进制位的 3 3 3 0 0 0或者 3 3 3 1 1 1,它们都是 3 3 3的倍数。因此答案的第 i i i个二进制位就是数组中所有元素的第 i i i个二进制位之和除以 3 3 3的余数

时间复杂度: O ( n l o g C ) O(nlogC) O(nlogC),其中 n n n是数组长度, C C C是元素的数据范围,本题 l o g C = l o g 32 = 32 logC = log^{32} = 32 logC=log32=32
空间复杂度: O ( 1 ) O(1) O(1)

class Solution {
public:
    int singleNumber(vector<int> &nums) {
        int res = 0;
        for (int i = 0; i < 32; i++) {
            int total = 0;
            for (auto c: nums) total += c >> i & 1;
            if (total % 3) res |= (1 << i);
        }
        return res;
    }
};

方法2:数字电路优化、状态机:不容易想到,可以看y总视频。

7.10 只出现一次的数字 III

LeetCode 260

恰好有两个元素只出现一次,其余所有元素均出现两次

如果将所有的数异或起来,那么结果就是最终两个仅出现一次的数,记为 a 、 b a、b ab,因为两个数不同,所以 a   X O R   b ≠ 0 a \ XOR \ b \ne 0 a XOR b=0,就意味着 a 、 b a、b ab的二进制表示中至少有一位不同。假设 a 、 b a、b ab的二进制表示中的第 k k k位不同,且 a a a的第 k k k位为 0 0 0 b b b的第 k k k位为 1 1 1,那么就可以将数组中的所有数划分为两类:

  • 第一类:数组中元素二进制表示第 k k k位为 0 0 0 a a a包含在里面,并且该类其余元素均出现两次;
  • 第二类:数组中元素二进制表示第 k k k位为 1 1 1 b b b包含在里面,并且该类其余元素均出现两次;

对于每一类求解仅出现一次的元素就可以使用 7.8 题的思路。

class Solution {
public:
    int get(vector<int> &nums, int k, int t) {
        int res = 0;
        for (auto x: nums)
            if ((x >> k & 1) == t) res ^= x;
        return res;
    }

    vector<int> singleNumber(vector<int> &nums) {
        int ab = 0;
        for (auto x: nums) ab ^= x;
        int k = 0;
        while (!(ab >> k & 1)) k++;
        return {get(nums, k, 0), get(nums, k, 1)};
    }
};

7.11 求整数二进制表示下所有是 1 1 1的位

题目源自《算法进阶指南》中 P 10 P_{10} P10,尚未找到题源。

这个题目主要是想实现一下 l o g log log函数,因为在C++ math.h库的 l o g log log函数是以 e e e为底的实数运算,并且复杂度常数较大,所以我们需要预处理一个数组,通过 h a s h hash hash的方法代替 l o g log log运算。

n n n较小的时候,最简单的方法是直接建立一个数组 H H H,令 H [ 2 k ] = k H[2^k] = k H[2k]=k

const int N = 1 << 20;
int H[N + 1];
for (int i = 0; i <= 20; i ++ ) H[1 << i] = i;

n n n比较大的时候,可以建立一个长度为 37 37 37 的数组 H H H,令 H [ 2 k   m o d   37 ] = k H[2^k \ mod \ 37] = k H[2k mod 37]=k

数学Tips:对 ∀ k ∈ [ 0 , 35 ] , 2 k   m o d   37 \forall k\in [0,35], 2^k \ mod \ 37 k[0,35],2k mod 37 互不相等,且恰好取遍整数 1 ∼ 36 1 \sim 36 136

int H[37];
for (int i = 0; i < 36; i ++ ) H[(1ll << i) % 37] = i;

算法思路:

不断将 n n n 赋值为 n − l o w b i t ( n ) n - lowbit(n) nlowbit(n),直至 n = 0 n = 0 n=0 为止。其中获取的值 k = l o w b i t ( n ) k = lowbit(n) k=lowbit(n),对其做对数运算 l o g 2 k log_2^k log2k即为 1 1 1所在的位置。

程序完整代码:

#include 
#include 
using namespace std;

int H[37];
int n;

int main() {
    for (int i = 0; i < 36; i++) H[(1ll << i) % 37] = i;
    while (cin >> n) 
        while (n > 0) {
            cout << H[(n & -n) % 37] << ' ';
            n -= n & -n;
        }
    puts("");
    return 0;
}

8 离散化

TIps:这里特指整数的离散化,即有序的离散化,且保序(即表示离散化之后的序列依然是有序的)。

8.1 区间和

ACWing 802

这个题如果想使用差分数组来做的话是不行的,我们不可能开一个这么大的数组,所以离散化的优势就来了。

几个重要变量:

  • alls:用于存放操作位置x以及询问的区间端点l、r
  • add:用于存放操作位置x和操作数c
  • query:用于存放询问的区间端点l、r
  • a[i]:用于存放离散后的数;
  • s[i]:用于存放a[i]的前缀和。

一点新的理解(2022.4.12):离散化的操作,通过本题可以知道,实质上就是将要插入的位置和询问区间的端点值一起放入alls中,对alls进行去重和排序之后,就形成了这些端点对应于alls数组的从0开始的一系列下标了(如下图)。通过find找到这些下标,然后在利用a数组存储对应下标上的值,最后使用s数组求前缀和即可。
第一章 基础算法(1)_第6张图片
第一章 基础算法(1)_第7张图片

映射关系图:
第一章 基础算法(1)_第8张图片

更通俗的解释离散化就是通过将要一个可能很大范围的操作的下标和询问的区间端点映射到了一个从下标0开始的范围很小数组中。

#include 
#include 
#include 

using namespace std;

typedef pair<int, int> PII; // 存储每对操作数

const int N = 300010; // n次操作,最大1e5,m次询问,最大1e5次,一个询问两个数,共3e5

int n, m;
int a[N], s[N]; // a是存储的数,s是前缀和数组

vector<int> alls; // 存储所有要离散化数
vector<PII> add, query; // 分别对应插入和求解两个操作

// 离散化的目标就是是插入的值映射到1~alls.size()这个范围上
// 比如 1, 3, 100, 2000, 50000000 这几个数,就不能使用50000000这么大的数组来存储
// 而离散化就将其映射到 1, 2, 3, 4, 5 这几个数上
// 这个离散化的过程是使用二分查找其值在有序序列上的序号来实现的

// 离散化的本质是将数字本身key映射为它在数组中的索引index(1 开始)
// 所以通过二分求索引(value -> index)是离散化的本质

// 求x的值离散化之后的结果
// find() 函数的功能就是返回 alls 中 n+2m(也可能小于,因为可能有重复) 个数的下标 + 1
int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    
    return r + 1; // 使离散化的下标从1开始,方便求前缀和,因为前缀和从1开始比较好做
}

int main() 
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int x, c; // 在x的位置上加上c
        cin >> x >> c;
        add.push_back({x, c}); // 将数据输入add中
        
        alls.push_back(x); // 将位置输入待离散化的数组中
    }
    
    for (int i = 0; i < m; i ++ ) // 读入区间
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});
        
        // 将区间坐标加入待离散化的数组中去
        // 这一步很重要,不加入会出错!!!
        alls.push_back(l);
        alls.push_back(r);
    }
    
    // Tips:排序与去重
    // unique()函数返回去重后数组的最后一个数值的下标
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    
    // 处理插入 add 中包含的是 (x, c)
    // a[x] 存储离散化后的数值
    for (auto item : add)
    {
        int x = find(item.first); // 找出离散化后的坐标
        a[x] += item.second; // 离散化后要加的数
    }
    
    // 处理前缀和 
    // alls 中包含的是 (x, l, r)
    // 即 alls 中存放的是元素下标,a 中存放的是元素的值
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
    
    // 处理询问
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

简洁版:

#include 
#include 
#include 
using namespace std;

typedef pair<int, int> PII;
const int N = 3e5 + 10;

int n, m;
int s[N];
vector<int> alls;
vector<PII> add, query;

int find(int x) {
    int l = 0, r = alls.size() - 1;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 注意下标
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        int x, c; cin >> x >> c;
        add.push_back({x, c});
        alls.push_back(x);
    }
    for (int i = 0; i < m; i++) {
        int l, r; cin >> l >> r;
        query.push_back({l, r});
        alls.push_back(l); alls.push_back(r);
    }
    sort(alls.begin(), alls.end());
    alls.erase((unique(alls.begin(), alls.end())), alls.end());
    for (auto item: add) {
        int x = find(item.first);
        s[x] += item.second;
    }
    for (int i = 1; i <= alls.size(); i++) s[i] += s[i - 1];
    for (auto item: query) {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }
    return 0;
}

8.2 电影 (排序+离散化)

ACwing 103

对所有科学家会的语言、电影音频语言、电影字幕语言进行离散化,这个过程参照文章中的8.1。

后面就需要从所有的电影中选择出最喜欢的电影。

#include 
#include 
#include 
#include 
using namespace std;

#define ios \
        ios::sync_with_stdio(false);\
        cin.tie(0);\
        cout.tie(0)

const int N = 2e5 + 10;

int n, m;
int a[N], b[N], c[N]; // 科学家会的语言编号,电影音频、字幕语言编号
vector<int> alls; // 记录语言编号 3*N假设三个来源的语言均不相同
int ans[3 * N]; // 记录每种语言有多少科学家会

inline int find(int x) {
    int l = 0, r = alls.size() - 1;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;
}

int main() {
    ios;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
        alls.push_back(a[i]);
    }
    scanf("%d", &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d", b + i);
        alls.push_back(b[i]);
    }
    for (int i = 1; i <= m; i++) {
        scanf("%d", c + i);
        alls.push_back(c[i]);
    }
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());

    // 记录每种语言有多少科学家会
    for (int i = 1; i <= n; i++) ans[find(a[i])]++;

    //ans0保存最终结果,ans1和ans2为中间结果
    int ans0 = 0, ans1 = 0, ans2 = 0;

    //遍历所有电影,按要求找到最多科学家会的电影
    for (int i = 1; i <= m; i++) {
        int x = ans[find(b[i])], y = ans[find(c[i])];
        if (x > ans1 || x == ans1 && y > ans2)
            ans0 = i, ans1 = x, ans2 = y;
    }

    //如果所有的电影的声音和字幕的语言,科学家们都不懂,随便选一个
    if (!ans0) puts("1");
    else printf("%d\n", ans0);
    return 0;
}

你可能感兴趣的:(算法笔记,算法,c++)