蓝桥杯算法集训 - Week 1:二分、前缀和、差分、树状数组

蓝桥杯算法集训 - Week 1

本系列随笔用于整理AcWing题单——《蓝桥杯集训·每日一题2024》的系列题型及其对应的算法模板。

一、二分查找

二分算法原理复习参考:二分查找 - Hello 算法

Ⅰ、代码模板

static boolean 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) / 2;
        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) / 2;
        if (check(mid))
			l = mid;
        else
			r = mid - 1;
    }
    return l;
}

Ⅱ、分巧克力

1227. 分巧克力 - AcWing题库

分巧克力

import java.io.*;

public class Main {
	static final int N = 100010;
	static int n, k;
	static int[][] a = new int[N][2];

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String[] split = br.readLine().split(" ");
		n = Integer.parseInt(split[0]);
		k = Integer.parseInt(split[1]);
		for (int i = 0; i < n; i++) {
			split = br.readLine().split(" ");
			a[i][0] = Integer.parseInt(split[0]);
			a[i][1] = Integer.parseInt(split[1]);
		}
		br.close();
		
		// 二分枚举边长
		int l = 1, r = N;
		while (l < r) {
			int mid = l + (r - l + 1) / 2;
			if (check(mid)) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		System.out.println(l);
	}
	
	// 检查是否可以分出k块巧克力
	static boolean check(int mid) {
		int cnt = 0;
		for (int i = 0; i < n; i++) {
			cnt += (a[i][0] / mid) * (a[i][1] / mid);
			if (cnt >= k)
				return true;
		}
		return false;
	}
}

二、前缀和

前缀和原理复习参考:【优选算法】—— 前缀和算法

Ⅰ、代码模板

一维前缀和

// 初始化前缀和
S[i] = a[1] + a[2] + ... a[i]
// 求区间[l, r]的和
a[l] + ... + a[r] = S[r] - S[l - 1]

二维前缀和

// 初始化前缀和数组:第 i 行 j 列格子左上部分所有元素的和;
S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j];
// 计算以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和
S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1];

Ⅱ、一维数组的动态和

给你一个数组 nums。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。

请返回 nums 的动态和。

public class Main{
    public int[] runningSum(int[] nums) {
        int[] prefix = new int[nums.length];
        prefix[0] = nums[0];
        for (int i = 1; i < nums.length; ++i) {
            prefix[i] = prefix[i - 1] + nums[i];
        }
        return prefix;
    }
}

Ⅲ、统计子矩阵

4405. 统计子矩阵 - AcWing题库

统计子矩阵

import java.io.*;

public class Main {
	static final int N = 510;
	static int n, m, k;
	static int[][] martix = new int[N][N];
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String[] split = br.readLine().split(" ");
		n = Integer.parseInt(split[0]);
		m = Integer.parseInt(split[1]);
		k = Integer.parseInt(split[2]);
		for (int i = 1; i <= n; i++) {
			split = br.readLine().split(" ");
			for (int j = 1; j <= m; j++) {
                // 计算矩阵的前缀和数组
				martix[i][j] = martix[i - 1][j] + martix[i][j - 1] - martix[i - 1][j - 1] + Integer.parseInt(split[j - 1]);
			}
		}
		br.close();
		
		long res = 0;
		for (int x1 = 1; x1 <= n; x1++) {
			for (int y1 = 1; y1 <= m; y1++) {
				for (int x2 = x1; x2 <= n; x2++) {
                    for (int y2 = y1; y2 <= m; y2++) {
                        // 无优化枚举每一个二维子矩阵的前缀和——O(n^4)
                        if (martix[x2][y2] - martix[x1 - 1][y2] - martix[x2][y1 - 1] + martix[x1 - 1][y1 - 1] <= k)
							res++;
                    }
				}
			}
		}
		System.out.println(res);
	}
}

三、差分

差分算法复习参考:【详解】手撕 一维、二维、三维差分数组原理(附图解,模板,例题分析)

注:差分算法实际为《蓝桥杯集训·每日一题2024》Week 2 内容。但差分与前缀和算法有着很大联系,互为逆运算,故提前放置于本章。

Ⅰ、代码模板

一维差分

// 初始化一维差分
B[n] = a[n] - a[n - 1];

// 给区间[l, r]中的每个数加上 c
B[l] += c, B[r + 1] -= c;

二维差分

// 初始化二维差分
S[i][j] = a[i][j] − a[i−1][j] − a[i][j−1] + a[i−1][j−1];

// 给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上 c:
S[x1][y1] += c;
S[x2 + 1][y1] -= c;
S[x1][y2 + 1] -= c;
S[x2 + 1][y2 + 1] += c;

Ⅱ、借教室

503. 借教室 - AcWing题库

借教室

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String[] split = reader.readLine().split(" ");
        int n = Integer.parseInt(split[0]);
        int m = Integer.parseInt(split[1]);
        int[] count = new int[n + 1];
        String[] cns = reader.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            count[i] = Integer.parseInt(cns[i-1]);
        }
        // 初始化差分数组
        for (int i = n ; i >= 1; i--){
            count[i] -= count[i-1];
        }
        int[][] orders = new int[m + 1][3];
        for (int i = 1; i <= m; i++) {
            String[] ods = reader.readLine().split(" ");
            orders[i][0] = Integer.parseInt(ods[0]);
            orders[i][1] = Integer.parseInt(ods[1]);
            orders[i][2] = Integer.parseInt(ods[2]);
        }
        reader.close();
        
        // 二分查找最后符合要求的日期
        int l = 1, r = m;
        while (l < r){
            int mid = (l + r) / 2;
            if (check(count, orders, mid)){
                l = mid + 1;
            }else {
                r = mid;
            }
        }
        if (check(count, orders, r)) {
            System.out.println(0);
        } else {
            System.out.println(-1);
            System.out.println(r);
        }
    }

    // 使用差分算法优化计算效率
    private static boolean check(int[] count, int[][] orders, int mid) {
        // 复制一份差分数组
        long[] copy = new long[count.length];
        for (int i = 1; i < copy.length; i++) {
            copy[i] = count[i];
        }
        // 将第一天到第mid天的教室减去
        for (int i = 1; i <= mid ; i++) {
            // orders[i][0]为第i个订单所需教室数、orders[i][1]为起始天数、orders[i][2]为结束天数
            copy[orders[i][1]] -= orders[i][0];
            if (orders[i][2] != copy.length - 1){
                copy[orders[i][2] + 1] += orders[i][0];
            }
        }
        long res = 0;
        for (int i = 1; i < copy.length; i++) {
            res += copy[i]; // 对差分数组求前缀和获得原始数据
            if (res < 0){
                return false;
            }
        }
        return true;
    }
}

Ⅲ、棋盘

5396. 棋盘 - AcWing题库

棋盘

import java.io.*;

public class Main {
	static final int N = 2010; 
	static int n, m;
	static int matrix[][] = new int[N][N];
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		PrintWriter pw = new PrintWriter(System.out);
		String[] split = br.readLine().split(" ");
		n = Integer.parseInt(split[0]);
		m = Integer.parseInt(split[1]);
		for (int i = 1; i <= m; i++) {
			split = br.readLine().split(" ");
			// 初始化二维差分
			insert(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]), Integer.parseInt(split[3]));
		}
		br.close();
		
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				// 求二维前缀和获取结果
				matrix[i][j] += matrix[i - 1][j] + matrix[i][j-1] - matrix[i - 1][j - 1];
			}
		}
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				if (matrix[i][j] % 2 == 0) pw.print("0");
				else pw.print("1");;
			}
			pw.print("\n");
		}
		pw.close();
	}
	
	// 对二维差分进行修改的代码模板
	static void insert(int x1, int y1, int x2, int y2) {
		matrix[x1][y1]++;
		matrix[x1][y2 + 1]--;
		matrix[x2 + 1][y1]--;
		matrix[x2 + 1][y2 + 1]++;
	}
}

四、树状数组

树状数组是一种数据结构,可以快速地完成以下两个操作:

  • 第 i 个数加上 c
  • 快速求前缀和,即任意区间[i,j]的和

Ⅰ、代码模板

// 树状数组长度是固定的,为 n+1
// 树状数组的下标必须从 1 开始
static int[] tr = new int[n + 1];

// 求最低的一位 1
static int lowbit(int x){
    return x & -x;
}

// 在 tr[x] 的位置加上c
static void add(int x, int c){
    for(int i = x; i <= n; i += lowbit(i))
		  tr[i] += c;
}

// 查询前缀和
static int query(int x){
    int res = 0;
    for(int i = x; i > 0; i -= lowbit(i))
		  res += tr[i];
    return res;
}

Ⅱ、动态求连续区间和

1264. 动态求连续区间和 - AcWing题库

动态求连续区间和

import java.io.*;

public class Main {
	static final int N = 100010;
	static int n, m;
	static int[] a = new int[N], tr = new int[N];
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String[] split = br.readLine().split(" ");
		n = Integer.parseInt(split[0]);
		m = Integer.parseInt(split[1]);
		split = br.readLine().split(" ");
		for (int i = 1; i <= n; i++) {
			a[i] = Integer.parseInt(split[i - 1]);
		}
		
		// 初始化 tr 数组
		for (int i = 1; i <= n; i++) {
			 add(i, a[i]);
		}
		for (int i = 0; i < m; i++) {
			split = br.readLine().split(" ");
			int o = Integer.parseInt(split[0]);
			int a = Integer.parseInt(split[1]);
			int b = Integer.parseInt(split[2]);
			if (o == 0) {
				System.out.println(query(b) - query(a - 1)); // 通过 tr 前缀和求区间和
			} else {
				add(a, b); // 修改单个元素
			}
		}
		br.close();
	}
	
	static int lowbit(int x) {
		return x & -x;
	}
	
	static void add(int x, int c) {
		for (int i = x; i <= n; i += lowbit(i)) {
			tr[i] += c;
		}
	}
	
	static int query(int x) {
		int res = 0;
		for (int i = x; i > 0; i -= lowbit(i)) {
			res += tr[i];
		}
		return res;
	}
}
posted @ 2024-03-10 22:25  TfiyuenLau  阅读(62)  评论(0编辑  收藏  举报