【基础算法】二分,贪心等 学习笔记

普及组基础算法

这些都是零零散散接触过的基础算法,写个笔记把这些整理到一起来。

线性降维技巧

之前在学校洛谷团队里看到一个题单,觉得这些技巧可能有用,就转存了。

前缀和 差分

前缀和是一种对区间求和问题进行降维的方法。具体地,对于给定数组 $ A[n] $,求出 \(A[l,r]\) 区间和这个问题,朴素的方法时间复杂度为 \(O(n)\),使用前缀和进行一次 \(O(n)\) 的预处理后,可以 \(O(1)\) 进行查询。
实现方法:

int n;
int a[100],f[100]; // f[] 为预处理数组,f[i] 表示 a[1] 到 a[i] 的和

cin >> n;
for (int i=1;i<=n;i++){
    cin >> a[i];
    f[i]=f[i-1]+a[i]; // 预处理:前缀和的递推计算方法
}

// 查询:a[l,r]的和是:f[r]-f[l-1]

前缀和也可以推广到二维。具体的,对于给定二维数组 $ A[n][m] $,求出其中 \(A[xl,xr][yl,yr]\) 的子矩阵和这个问题,朴素的方法时间复杂度为 \(O(nm)\), 也可以使用前缀和进行优化,需要用到容斥原理。

普及组不需要用到很复杂的数学知识,容斥原理只需要小学数学的那些就可以了,这里做一个对二维容斥原理的简单的介绍:给定集合 \(A\) 和 集合 \(B\),则有:\(|A \cup B| = |A| + |B| - |A \cap B|\)

容斥原理不局限于集合大小,也可以帮助我们求前缀和。可以通过下面方法实现二维前缀和。

int n,m;
int a[100][100], f[100][100]; // f[i][j] 表示 f[1,i][1,j] 的和

cin >> n >> m;
for (int i=1;i<=n;i++)
    for (int j=1;j<=m;j++){
        cin >> a[i][j];
        f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+a[i][j];
    }

int query(int xl, int xr, int yl, int yr) {
    return f[xr][yr] - f[xr][yl - 1] - f[xl - 1][yr] + f[xl - 1][yl - 1];
}

差分可以视作前缀和的逆运算。具体地,对于给定数组 \(A[n]\),定义其差分数组为 \(d[n]\),其中 \(d[i]=a[i]-a[i-1],(d[1]=a[1])\)。前缀和被用于区间查询,而差分被用于区间修改。使用差分后,可以在 \(O(1)\) 时间内进行区间加减(将 \(a[i,j]\) 内所有值增加 \(k\)),具体代码如下:

洛谷 P2367 语文成绩(差分模板)

#include <bits/stdc++.h>
using namespace std;
int n,p;
int a[5000005],b[5000005],ans=200;
int x,y,z;

int main(){
    ios::sync_with_stdio(0);
    cin >> n >> p;
    for (int i=1;i<=n;i++){ // 差分初始化,其中 b[] 为差分数组
        cin >> a[i];
        b[i]=a[i]-a[i-1]; 
    }
    for (int i=1;i<=p;i++){
        cin >> x >> y >> z;
        b[x]+=z,b[y+1]-=z; // 区间加
    }
    int s=0;
    for (int i=1;i<=n;i++){
        s+=b[i]; // 差分的前缀和数组就是原数组,所以称其为逆运算
        ans=min(ans,s);
    }
    cout << ans << endl;
    return 0;
}

例题

洛谷 P1387 最大正方形

求出原矩阵的二维前缀和 s[][],对于一个边长为 \(k\) 的子正方形矩阵 s[i,i+k-1][j,j+k-1],如果其和为 $ k^2 $ 则中间不含 $ 0 $。枚举边长和起点,时间复杂度为 $ O(n^3) $,可以通过给定数据。

代码实现如下:

#include <bits/stdc++.h>
using namespace std;
// #define int long long

int n, m;
int a[105][105], f[105][105];

int query(int xl, int xr, int yl, int yr) {
    return f[xr][yr] - f[xr][yl - 1] - f[xl - 1][yr] + f[xl - 1][yl - 1];
}

signed main() {
    #ifndef ONLINE_JUDGE
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // keep your hard-working, %%% ftc
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];
        }
    
    int mn=min(m,n);
    for (int k=2;k<=mn;k++){
        bool flag=0;
        for (int i=1;i+k-1<=n;i++){
            for (int j=1;j+k-1<=m;j++){
                flag|=(query(i,i+k-1,j,j+k-1)==k*k);
            }
        }
        if (flag==0){
            cout << k-1 << endl;
            break;
        }
    }

    // keep your hard-working, %%% ftc

    #ifndef ONLINE_JUDGE
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

洛谷 P3406 海底高铁

首先考虑何时买卡,设第 $ i $ 段铁路被乘坐了 $ x_i $ 次,当 $ x_i \times a[i] > x_i \times b[i] + c[i] $ 时,买卡可以节省 $ x_i \times a[i] - (x_i \times b[i] + c[i]) $ 元,此时应该买卡。

接着考虑如何计算 $ x_i $,当从第 \(a\) 站坐到第 \(b\) 站时($ a < b $),需要购买第 \(a\) 条铁路到第 \(b-1\) 条的车票各 $ 1 $ 张。区间加可以使用差分实现。

最后计算出全部买票的钱和节省的钱(全部买票的钱可以用前缀和优化计算),相减即为答案。

参考代码:

#include <iostream>
#include <algorithm>
#include <cmath>
#include <ctime>
using namespace std;
#define int long long

const int N = 1e5 + 5;

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

signed main() {

#ifndef ONLINE_JUDGE
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
#endif

    // keep your hard-working, %%% ftc

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        cin >> p[i];
    }
    for (int i = 1;i <= n - 1;i++) {
        cin >> a[i] >> b[i] >> c[i];
        fa[i] = fa[i - 1] + a[i];
    }
    for (int i = 2;i <= m;i++) {
        int start = p[i - 1], to = p[i];
        if (start < to) {
            ct[start]++, ct[to]--;
            ans += fa[to - 1] - fa[start - 1];
        }
        else /* start > to */ {
            ct[to]++, ct[start]--;
            ans += fa[start - 1] - fa[to - 1];
        }
    }
    int x = 0;
    for (int i = 1;i <= n - 1;i++) {
        x += ct[i];

        if (c[i] + x * b[i] < x * a[i]) {
            ans -= (x * a[i] - (c[i] + x * b[i]));
        }
    }

    cout << ans << endl;


    // keep your hard-working, %%% ftc

#ifndef ONLINE_JUDGE
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
#endif
    return 0;
}

离散化

离散化是排序算法对于线性降维的一个重要应用。通常情况下,如果一组数值域很大,但是只考虑他们的大小关系,则可以通过离散化的方式将值域降维到不重复元素个数。

比如:$ [1,20,300,4234,51234,64321,114514,1919810] $ 这组数,如果只考虑他们的大小关系,则和 $ [1,2,3,4,5,6,7,8] $ 等价。将数组排序后就可以用元素所在的位置代表这个元素。

离散化的实现方法:

int n, m = 1;
int a[], b[]; // a[]为原数组, b[]是离散化后的数组

void init() {
    sort(a + 1, a + 1 + n); // 先对数组排序
    b[1] = a[1];
    for (int i = 2;i <= n;i++) {
        if (a[i] != a[i - 1]) b[++m] = a[i];
    }
    // 或STL: m = unique(a + 1, a + 1 + n) - a;
}

int query(int x) {
    return lower_bound(b + 1, b + m + 1, x) - b;
}

离散化还可以通过 STL:map 实现。

例题:CF670C Cinema

把语言离散化,统计懂第 \(i\) 种语言的科学家有几位,然后比较每个电影能让几位科学家愉悦,选出最多的即可。
参考代码(很久之前写的,很丑):

#include <bits/stdc++.h>
using namespace std;
int n,m,k,d,a[200005],b[200005],c[200005],l[600010],q[600010];
int cnt[600010];
int ans=1;
int query(int x){
    return lower_bound(q+1,q+d+1,x)-q;
}

signed main(){
    ios::sync_with_stdio(0);
    cin >> n;
    for (int i=1;i<=n;i++){
        cin >> a[i];
        l[++k]=a[i];
    }
    cin >> m;
    for (int i=1;i<=m;i++){
        cin >> b[i];
        l[++k]=b[i];
    }
    for (int i=1;i<=m;i++){
        cin >> c[i];
        l[++k]=c[i];
    }
    sort(l+1,l+1+k);
    for (int i=1;i<=k;i++){
        if (l[i]!=l[i-1])q[++d]=l[i];
    }
    for (int i=1;i<=n;i++){
        cnt[query(a[i])]++;
    }
    for (int i=1;i<=m;i++){
        int bt=query(b[i]),ct=query(c[i]);
        if (cnt[bt]>cnt[query(b[ans])]){
            ans=i;
        } else if (cnt[bt]==cnt[query(b[ans])]&&cnt[ct]>cnt[query(c[ans])]){
            ans=i;
        }
    }
    cout << ans << endl;
    return 0;
}

有一道离散化和并查集结合的题:洛谷 P1955 [NOI2015] 程序自动分析

双指针

单调栈 单调队列

二分

二分算法可以解决的问题具有两段性的特点,直观地说,在一个区间中,如果存在一个性质,区间内一半数具有这个性质,另外一半没有,则可以通过二分法找出第一个(或最后一个,对于前半边具有性质)具有性质的数。二分算法主要有整数二分和实数二分两种。

整数二分

如果在整数区间 $ [l,r] $ 中,$ [l,x] $ 这一段具有某种性质而 $ [x+1,r] $ 不具有,或者 $ [x,r] $ 这一段具有某种性质而 $ [l,x-1] $ 不具有,则整数二分可以求出这个 \(x\) 的值。

代码模板

对于上述两种情况,给出以下两段代码:

bool check(int x); // 检验某个数是否具有某个性质

int solve_r(int l, int r) { // 右半边具有某种性质
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}

int solve_l(int l, int r) { // 左半边具有某种性质
    while (l < r) {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

对于中间值是否具有性质,分成两种情况,分别缩小区间范围。每次缩小一半,则算法的时间复杂度为 $ O(\log n) $

例题

  • 洛谷 P1873 [COCI2011-2012#5] EKO / 砍树
  • P1182 数列分段 Section II
  • P1314 [NOIP2011 提高组] 聪明的质监员
  • P1083 [NOIP2012 提高组] 借教室
  • P4343 [SHOI2015] 自动刷题机

实数二分

当答案要求精确到小数点的后 $ k $ 位时,通常取 $ eps = 10^{-(k+2)} $,然后类似整数二分,写出以下代码,就可以求出近似值。

bool check(double x); // 检验某个数是否具有某个性质

int solve(double l, double r) {
    while (l + eps < r) {
        int mid = (l + r)/2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    return l;
}

还有一种固定循环次数的技巧,来实现实数二分,精确度也很高。

int solve(double l, double r) {
    for (int i = 1; i <= 30; i++) {
        int mid = (l + r)/2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    return l;
}

例题

  • 洛谷 P1024 [NOIP2001 提高组] 一元三次方程求解

二分查找

  • 头文件:<algorithm>
  • lower_bound(begin, end, val) 在[begin, end) 区间找 val 第一次出现的地址。
  • upper_bound(begin, end, val) 在[begin, end) 区间找大于 val 的数第一次出现的地址(相当于 val 最后一次出现的地址的后一个)。

例题:洛谷 P1102 A - B = C

贪心

贪心是解决一类题目的思想,总体思路是用局部最优解推理全局最优解。贪心的思考方法有:反证法、数学归纳法等。

posted @ 2023-06-22 10:06  蒟蒻OIer-zaochen  阅读(163)  评论(0编辑  收藏  举报