Acwing - 算法基础课 - 笔记(二)

756 阅读8分钟

基础算法(二)

这节讲的是高精度前缀和,和差分

高精度

  • A + B:两个大整数相加
  • A - B:两个大整数相减
  • A × b:一个大整数乘一个小整数
  • A ÷ b:一个大整数除以一个小整数

A × BA ÷ B 见的不多,课程中没有讲。

大整数的存储:用一个数组来存大整数的每一位上的数。

这里将大整数的个位,存到数组的第一位,大整数的最高位,存到数组的最后一位,即采用小端序。

高精度加法

  • A + B

    算法题目:Acwing-791: 高精度加法

    高精度加法的流程,就是模拟人手动做加法的过程,将每一位依次相加,并带上前一位的进位。

    // C++
    #include <iostream>
    #include <vector>
    #include <string>
    using namespace std;
    
    vector<int> add(vector<int> &a, vector<int> &b) {
        int c = 0; // 进位
        vector<int> res;
        for(int i = 0; i < a.size() || i < b.size(); i++) {
            if(i < a.size()) c += a[i];
            if(i < b.size()) c += b[i];
            res.push_back(c % 10);
            c /= 10;
        }
        // 若最高位有进位
        if(c) res.push_back(c);
        return res;
        
    }
    
    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> res = add(A, B);
        for(int i = res.size() - 1; i >= 0; i--) printf("%d", res[i]);
    }
    
    // java
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
    	private static List<Integer> add(List<Integer> num1, List<Integer> num2) {
    		int c = 0; // 进位
    		List<Integer> res = new ArrayList<>();
    		for (int i = 0; i < num1.size() || i < num2.size(); i++) {
    			if (i < num1.size()) c += num1.get(i);
    			if (i < num2.size()) c += num2.get(i);
    			res.add(c % 10);
    			c /= 10;
    		}
    		// 加完, 看有无进位
    		if (c > 0) res.add(c);
    		return res;
    	}
    
    	public static void main(String[] args) throws IOException {
    		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    		String n1, n2;
    		n1 = reader.readLine();
    		n2 = reader.readLine();
    		List<Integer> num1 = new ArrayList<>(), num2 = new ArrayList<>();
    		for (int i = n1.length() - 1; i >= 0; i--) num1.add(n1.charAt(i) - '0');
    		for (int i = n2.length() - 1; i >= 0; i--) num2.add(n2.charAt(i) - '0');
    		List<Integer> res = add(num1, num2);
    		for (int i = res.size() - 1; i >= 0 ; i--) System.out.print(res.get(i));
    	}
    }
    

高精度减法

  • A - B

    先判断一下AB的相对大小,若

    • A >= B,则直接计算减法
    • A < B,计算B - A,并在结果前面加上负号-

    高精度减法的流程,也是模拟人手动做减法的流程,当某一位不够减时,会有借位的概念

    算法题目:Acwing-792: 高精度减法

    // C++
    #include<iostream>
    #include<vector>
    #include<string>
    
    using namespace std;
    // 比较 A 和 B 的大小, 当 A >= B 时, 返回true, 否则返回false
    bool cmp(vector<int> &A, vector<int> &B) {
        if(A.size() != B.size()) return A.size() > B.size(); // 长度不相等时, 直接比较A和B的长度即可
        else {
            for(int i = A.size() - 1; i >= 0; i--) {
                if(A[i] != B[i]) return A[i] > B[i]; // 长度相等时, 从最高位开始进行比较, 只要有一位不同, 就能得出大小关系
            }
            return true; // A和B相等
        }
    }
    
    vector<int> sub(vector<int> &A, vector<int> &B) {
        vector<int> C;
        // 用一个变量t来表示借位
        for(int i = 0, t = 0; i < A.size(); i++) {
            t = A[i] - t;   // 该位的运算结果为 : A[i] - B[i] - t , 即2数相减, 再减借位
            if(i < B.size()) t -= B[i];
            C.push_back((t + 10) % 10); // 当运算结果为负数, 则需要+10, 先+10再模10, 能够涵盖运算结果为整数或负数2种情况
            if(t < 0) t = 1; // 当原始运算结果为负数时, 表示需要借位, 将 t 置为 1
            else t = 0; // 否则将借位 t 重新置为 0 
        }
        // 去除前缀的0。 当长度 > 1 , 且最高位为 0 时, 移除最高位
        while(C.size() > 1 && C.back() == 0) 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');
        
        if(cmp(A, B)) {
            vector<int> C = sub(A, B);
            for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
        } else {
            vector<int> C = sub(B, A);
            printf("-");
            for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
        }
    }
    
    // java
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
    	public static List<Integer> sub(List<Integer> num1, List<Integer> num2) {
    		List<Integer> res = new ArrayList<>();
    		for (int i = 0, t = 0; i < num1.size(); i++) {
    			t = num1.get(i) - t;
    			if (i < num2.size()) t -= num2.get(i);
    			res.add((t + 10) % 10);
    			if (t < 0) t = 1;
    			else t = 0;
    		}
    		// 去掉前缀的0
    		while (res.size() > 1 && res.get(res.size() - 1) == 0) res.remove(res.size() - 1);
    		return res;
    	}
    
    	// 当 num1 >= num2, 返回 true, 否则返回 false
    	public static boolean cmp(List<Integer> num1, List<Integer> num2) {
    		if (num1.size() != num2.size()) return num1.size() > num2.size();
    		else {
    			for (int i = num1.size() - 1; i >= 0 ; i--) {
    				if (num1.get(i).intValue() != num2.get(i).intValue()) return num1.get(i) > num2.get(i);
    			}
    			return true; // num1 == num2
    		}
    	}
    
    	public static void main(String[] args) throws IOException {
    		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    		String n1, n2;
    		n1 = reader.readLine();
    		n2 = reader.readLine();
    		List<Integer> num1 = new ArrayList<>(), num2 = new ArrayList<>();
    		for (int i = n1.length() - 1; i >= 0; i--) num1.add(n1.charAt(i) - '0');
    		for (int i = n2.length() - 1; i >= 0; i--) num2.add(n2.charAt(i) - '0');
    		
    		if (cmp(num1, num2)) {
    			List<Integer> res = sub(num1, num2);
    			for (int i = res.size() - 1; i >= 0 ; i--) System.out.print(res.get(i));
    		} else {
    			List<Integer> res = sub(num2, num1);
    			System.out.print("-");
    			for (int i = res.size() - 1; i >= 0 ; i--) System.out.print(res.get(i));
    		}
    	}
    }
    

高精度乘法

  • A × b

    b看成一个整体,和A的每一位去做乘法。例如:123 × 12

    首先从A的最低位开始,计算3 × 12,得36,则结果中的个位为:36 % 10 = 6,产生的进位为36 / 10 = 3;继续下一位,计算2 × 12,得24,加上进位3,得27,结果中的十位为:27 % 10 = 7,产生的进位是27 / 10 = 2;继续下一位,1 × 12 + 2 = 14,则百位上的结果为14 % 10 = 4,产生进位14 / 10 = 1,则最终结果为1476

    算法题目:Acwing-793: 高精度乘法

    // C++
    #include<iostream>
    #include<vector>
    #include<string>
    using namespace std;
    
    vector<int> multi(vector<int> &A, int b) {
        vector<int> C;
        int t = 0;
        for(int i = 0; i < A.size(); i++) {
            t += b * A[i];
            C.push_back(t % 10);
            t /= 10;
        }
        if(t > 0) C.push_back(t);
        // 去掉前缀0
        while(C.size() > 1 && C.back() == 0) 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');
        
        vector<int> C = multi(A, b);
        
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
    }
    
    // java
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    	
    	public static List<Integer> multi(List<Integer> num1, int num2) {
    		int t = 0;
    		List<Integer> res = new ArrayList<>();
    		for (int i = 0; i < num1.size(); i++) {
    			t += num1.get(i) * num2;
    			res.add(t % 10);
    			t /= 10;
    		}
    		if (t > 0) res.add(t);
    		while (res.size() > 1 && res.get(res.size() - 1) == 0) res.remove(res.size() - 1);
    		return res;
    	}
    
    	public static void main(String[] args) throws IOException {
    		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    		String n1 = reader.readLine();
    		int n2 = Integer.parseInt(reader.readLine());
    		List<Integer> num1 = new ArrayList<>();
    		for (int i = n1.length() - 1; i >= 0; i--) num1.add(n1.charAt(i) - '0');
    		List<Integer> res = multi(num1, n2);
    		for (int i = res.size() - 1; i >= 0 ; i--) System.out.print(res.get(i));
    	}
    }
    

高精度除法

  • A ÷ b

    算法的流程,是模拟人手动做除法的流程。从最高位除起,依次做商和取余。每一轮的余数,乘以10,加上下一位的数,作为下一轮的被除数。

    比如123 ÷ 12

    算法题目:Acwing-794: 高精度除法

    // C++
    #include<iostream>
    #include<vector>
    #include<string>
    #include<algorithm>
    using namespace std;
    
    // r, 返回的余数, 这里是传引用
    vector<int> div(vector<int> &A, int b, int &r) {
        vector<int> C;
        r = 0; // 初始余数为0
        for(int i = A.size() - 1; i >= 0; i--) {
            // 除法从最高位开始
            r = r * 10 + A[i]; // 该轮的被除数
            C.push_back(r / b); // 被除数 除以 b 的商
            r = r % b; // 对 b 取余
        }
        // 此时的 C , 是从高位到低位存的, 要将 vector 反转一下
        reverse(C.begin(), C.end());
        // 去除前缀0
        while(C.size() > 1 && C.back() == 0) C.pop_back();
        return C;
    }
    
    int main() {
        string a;
        int b;
        vector<int> A;
        cin >> a >> b;
        for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
        int r = 0;
        vector<int> C = div(A, b, r);
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
        printf("\n%d\n", r);
    }
    
    // java
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class Main {
    
    	static class Pair {
    		List<Integer> quotient;
    		int remainder;
    
    		public Pair(List<Integer> quotient, int remainder) {
    			this.quotient = quotient;
    			this.remainder = remainder;
    		}
    	}
    
    	public static Pair div(List<Integer> num1, int num2) {
    		List<Integer> quotient = new ArrayList<>();
    		int r = 0;
    		for (int i = num1.size() - 1; i >= 0 ; i--) {
    			r = r * 10 + num1.get(i);
    			quotient.add(r / num2);
    			r = r % num2;
    		}
    		// 反转
    		Collections.reverse(quotient);
    		// 去除前缀0
    		while (quotient.size() > 1 && quotient.get(quotient.size() - 1) == 0) quotient.remove(quotient.size() - 1);
    		return new Pair(quotient, r);
    	}
    
    	public static void main(String[] args) throws IOException {
    		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    		String n1 = reader.readLine();
    		int n2 = Integer.parseInt(reader.readLine());
    		List<Integer> num1 = new ArrayList<>();
    		for (int i = n1.length() - 1; i >= 0; i--) num1.add(n1.charAt(i) - '0');
    		Pair res = div(num1, n2);
    		for (int i = res.quotient.size() - 1; i >= 0 ; i--) System.out.print(res.quotient.get(i));
    		System.out.printf("\n%d\n", res.remainder);
    	}
    }
    

前缀和

一维前缀和

假设有一个数组:a1,a2,a3,a4,a5,......,an(注意,下标从1开始)

前缀和 Si = a1 + a2 + a3 + .... + ai (即前i个数的和)

显然的,前缀和满足 Si = Si-1 + ai ,特殊的,我们可以定义S0 = 0,这样,任何区间[l, r],我们都可以用 Sr - Sl-1 这样的公式来计算,而不需要对边界进行特殊处理(当l = 1时,求[l, r]的所有数的和,其实就是求 Sr)。

前缀和的最大作用,就是用来求任意一段区间的所有数的和。比如,要求区间[l, r]的全部元素的和。若没有前缀和数组,则我们需要遍历原数组,时间复杂度为O(n)。若有前缀和数组,我们只需要计算 Sr - Sl-1,时间复杂度为O(1)。 因为 Sr = a1 + a2 + a3 + .... + al-1 + al + ..... + ar

而 Sl-1 = a1 + a2 + a3 + .... + al-1

练习题:Acwing - 795: 前缀和

// C++
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int s[N];

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) {
        scanf("%d", &s[i]);
        s[i] += s[i - 1]; // 输入数组和计算前缀和放在一起
    };
    while(m--) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);
    }
    return 0;
}

二维前缀和

上面的数组前缀和可能过于简单,下面是进阶版:矩阵前缀和(二维)

假设有如下的矩阵

a11 ,a12,a13,a14,.......,a1n

a21,a22,a23,a24,......., a2n

.....

.....

am1,am2,am3,am4,.....,amn

前缀和 Sij 表示点 aij 及其左上角区域的所有数的和。

经过简单推导(面积计算),可以得到 Sij = Si-1,j + Si,j-1 + aij - Si-1,j-1

若要计算左上角边界点为[x1, y1],右下角点为[x2, y2],这2个点之间部分的子矩阵的和(也是求任意一段区间内所有数的和),经过简单推导,能够得到下面的公式

S = Sx2,y2 - Sx1-1,y2 - Sx2,y1-1 + Sx1-1,y1-1(由于矩阵中是离散的点,所以计算时边界需要减掉1)

练习题:Acwing - 796: 子矩阵的和

// C++
#include<iostream>
using namespace std;

const int N = 1010;
int a[N][N];
int n, m, q;

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", &a[i][j]);
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1]; // 输入和计算前缀和同时进行
        }
    }
    
    while(q--) {
        int x1, x2, y1, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", a[x2][y2] - a[x1 - 1][y2] - a[x2][y1 - 1] + a[x1 - 1][y1 - 1]);
    }
    
    return 0;
}

小结:前缀和思想,是用来快速求解任意一段区间的所有数的和。时间复杂度O(1)

差分

差分,是前缀和的逆运算

一维差分

假设有一个数组,a1,a2,a3,a4,a5,......,an

针对这个数组,构造出另一个数组,b1,b2,b3,b4,b5,......,bn

使得a数组是b数组的前缀和,即使得 ai = b1 + b2 + .... + bi

此时,称b数组为a数组的差分

如何构造b数组呢:

b1 = a1,b2 = a2 - a1,b3 = a3 - a2,.....,bn = an - an-1

实际可以不用如此来构造,在输入数组a时,可以先假想数组a和数组b的全部元素都是0。然后每次进行一次插入操作(指的是对数组a[l, r]区间的每个数加上常数C),比如

a数组区间[1,1],加(插入)常数a1;对区间[2,2],加常数a2,.....,这样在输入数组a的同时,就能够快速构造出其差分数组b

差分的作用

若要对a数组中[l, r]区间内的全部元素都加上一个常数C,若直接操作a数组的话,时间复杂度是O(n)。而如果操作其差分数组b,则时间复杂度是O(1)。这是因为,数组a是数组b的前缀和数组,只要对 bl 这个元素加C,则a数组从l位置之后的全部数都会被加上C,但r位置之后的所有数也都加了C,所以我们通过对 br+1 这个数减去C,来保持a数组中r位置以后的数的值不变。

于是,对a数组的[l, r]区间内的所有数都加上一个常数C,就可以转变为对 blC,对br+1C

练习图:Acwing - 797: 差分

// C++
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int a[N]; // 使用一个数组即可

// 对[l, r]区间, 加上常数 c
void insert(int l, int r, int c) {
    // 直接操作差分数组
    a[l] += c;
    a[r + 1] -= c;
}

int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) { //注意下标从1开始
        int t;
        scanf("%d", &t);
        insert(i, i, t); // 构造差分数组
    }
    while(m--) {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    // 计算差分数组的前缀和, 还原原数组
    for(int i = 1; i <= n; i++) {
        a[i] += a[i - 1];
        printf("%d ", a[i]);
    }
}

二维差分

即差分矩阵

对于矩阵a,存在如下一个矩阵b

b11 ,b12,b13,b14,.......,b1n

b21,b22,b23,b24,......., b2n

.....

.....

bm1,bm2,bm3,bm4,.....,bmn

使得aij = 矩阵b[i, j]位置的左上角的所有数的和

称矩阵b为矩阵a的差分矩阵。

同样的,如果期望对矩阵a中左上角为[x1, y1],右下角为[x2, y2]的区域内的全部元素,都加一个常数C,则可以转化为对其差分矩阵b的操作。

先对b[x1, y1]位置上的元素加C,这样以来,a[x1, y1]这个点的右下角区域内的所有数都加上了C,但是这样就对[x2, y2]之后的区域也都加了C。我们对[x2, y2]之外的区域需要保持值不变,所以需要进行减法。对bx2+1,y1 减掉C,这样下图红色区域都被减了C,再对bx1,y2+1减掉C,这样下图蓝色区域都被减了C,而红色区域和蓝色区域有重叠,重叠的区域被减了2次C,所以要再加回一个C,即对bx2+1,y2+1 加上一个C。这样,就完成了对[x1, y1][x2, y2]区域内的所有数(下图绿色区域),都加上常数C

总结起来,对原矩阵a,在[x1, y1][x2, y2]区域内的全部元素加C,可以转换为对其差分矩阵b做如下操作

  • bx1,y1 + C
  • bx1,y2+1 - C
  • bx2+1,y - C
  • bx2+1,y2+1 + C

简单记忆为:对b[x1, y1]C,对[x2, y2]这个点,分别取x2 + 1y2 + 1(另一个轴则取y1x1),减C,然后对[x2 + 1, y2 + 1]C

构造矩阵b,采用与上面相同的方式,先假设矩阵a和矩阵b的元素全都为0,此时矩阵b是矩阵a的差分矩阵,依次进行插入操作即可。

即对矩阵a[1,1][1,1],加a[1][1],对[1,2][1,2],加a[1][2],........,如此即可构造出矩阵b

练习题:Acwing - 798: 差分矩阵

// C++
#include<iostream>
using namespace std;
const int N = 1010;
int a[N][N];
int n, m, q;

void insert(int x1, int y1, int x2, int y2, int c) {
    a[x1][y1] += c;
    a[x2 + 1][y1] -= c;
    a[x1][y2 + 1] -= c;
    a[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++) {
            int t;
            scanf("%d", &t);
            insert(i, j, i, j, t);
        }
    }
    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++) {
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
}

前缀和&差分 小结

  • 前缀和是用来:求任意区间的所有数的和,时间复杂度O(1)

  • 差分是用来:对任意区间内的所有数加上一个常数,时间复杂度O(1)

  • 前缀和差分互为逆运算

  • 前缀和,差分的题目,数组下标从1开始取,可以避免对边界特殊处理。

下面称S为a的前缀和数组(矩阵),或者称a为S的差分数组(矩阵)。

一维

前缀和 Si = Si-1 + ai,数组a任意区间[l, r]的和等于 Sr - Sl-1

差分 ai = Si - Si-1 ,但实际不会这样构造差分数组,直接定义一个函数 insert(int l, int r, int c),表示对S数组的[l, r]区间内的所有数加c。初始时,假想S数组和a数组全为0,对i = 1~n。每次调用insert(i, i, S[i]),即完成对差分数组a的构造。其中insert函数定义如下

void insert(int l, int r, int c) {
    // 对前缀和数组S的[l, r]区间的所有元素加上一个常数C, 只需要对S的差分数组a, 对a[l]加C, 对a[r + 1]减C
    a[l] += c;
    a[r + 1] -= c;
}

二维

前缀和 Si,j = Si-1,j + Si,j-1 - Si-1,j-1 + ai,j,矩阵a任意区间[x1, y1][x2, y2]的所有元素的和等于Sx2,y2 - Sx1-1,y2 - Sx2,y1-1 + Sx1-1,y1-1

简单记忆为,[x2, y2]的前缀和,减去,取 x2y2(另一个轴取y1 - 1x1 - 1)的前缀和,由于有重叠部分被减了2次,所以再加上[x1 - 1, y1 - 1]的前缀和

差分 ai,j ,这里也和一维差分一样的构造方式。直接定义一个函数insert(int x1, int y1, int x2, int y2, int c)。初始时,假想S矩阵和a矩阵全为0,对于每个点[i, j],每次调用insert(i, j, i, j, S[i][j]),即完成对差分矩阵a的构造。其中insert函数定义如下

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