二维差分
"二维差分" 使用: 二维差分的主要用处:快速地将一个区块中的所有元素都加上一个值 v。 使用差分可以将在数组 arr 上的区块操作转化为在差分数组 d 上的单点操作。转换方式如下: 假设区块左上角坐标为 (x1, y1),右下角坐标为 (x2, y2),对该区块中的每个元素都加上 v 等价于下面四个操作:
因为差分数组的前缀和相当于原数组,所以对差分数组进行以上四个单点操作后,就相当于给数组 arr 的区块加上一个值 v。
init: for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { arr[i][j] = scanner.nextInt(); insert(i, j, i, j, arr[i][j]); } } //相邻的符号相反,有x2/y2的有+1。 insert: d[x1][y1] += v; d[x2+1][y1] -= v; d[x1][y2+1] -= v; d[x2+1][y2+1] += v; // 计算二维差分的前缀和,即原二维数组 arr sum: arr[0][0] = d[0][0]; for (j = 1; j < m; j++){//第一行 arr[0][j] = arr[0][j - 1] + d[0][j];//类比一维 } for (i = 1; i < n; i++){//第一列 arr[i][0] = arr[i - 1][0] + d[i][0];//类比一维 } for (i = 1; i < n; i++){//其他 for (j = 1; j < m; j++){ arr[i][j] = arr[i - 1][j] + arr[i][j - 1] - arr[i - 1][j - 1] + d[i][j];//类比前缀和构建 } }
例题:ACWing 798. 差分矩阵
题目描述: 输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1, y1, x2, y2, c,其中 (x1, y1) 和 (x2, y2) 表示一个子矩阵的左上角坐标和右下角坐标。 每个操作都要将选中的子矩阵中的每个元素的值加上 c。 请你将进行完所有操作后的矩阵输出。 输入格式: 第一行包含整数 n, m, q。 接下来 n 行,每行包含 m 个整数,表示整数矩阵。 接下来 q 行,每行包含 5 个整数 x1, y1, x2, y2, c,表示一个操作。 输出格式: 共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。 数据范围: 1 ≤ n, m ≤1000, 1 ≤ q ≤100000, 0 ≤ x1 ≤ x2 ≤ n - 1, 0 ≤ y1 ≤ y2 ≤ m - 1, −1000 ≤ c ≤ 1000, −1000 ≤ 矩阵内元素的值 ≤ 1000 输入样例: 3 4 3 1 2 2 1 3 2 2 1 1 1 1 1 0 0 1 1 1 0 2 1 2 2 2 0 2 3 1 输出样例: 2 3 4 1 4 3 4 1 2 2 2 2
public class demo78_二维差分_差分矩阵 { static int n, m, q; static int[][] arr = new int[1000][1000]; static int[][] d = new int[1001][1001]; static BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); static Scanner scanner = new Scanner(System.in); static PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); public static void main(String[] args) throws IOException { n = scanner.nextInt(); m = scanner.nextInt(); q = scanner.nextInt(); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { arr[i][j] = scanner.nextInt(); insert(i, j, i, j, arr[i][j]); } } //进行q次操作 while (q-- > 0) { insert(scanner.nextInt(), scanner.nextInt(), scanner.nextInt(), scanner.nextInt(), scanner.nextInt()); } //计算二维差分的前缀和,即原二维数组 arr arr[0][0] = d[0][0]; for (int j = 1; j < m; j++) { arr[0][j] = arr[0][j - 1] + d[0][j]; } for (int i = 1; i < n; i++) { arr[i][0] = arr[i - 1][0] + d[i][0]; } for (int i = 1; i < n; i++) { for (int j = 1; j < m; j++) { arr[i][j] = arr[i - 1][j] + arr[i][j - 1] - arr[i - 1][j - 1] + d[i][j]; } } //输出 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { out.printf("%d ", arr[i][j]); } out.printf("\n"); } out.flush(); } static void insert(int x1, int y1, int x2, int y2, int c) { d[x1][y1] += c; d[x1][y2 + 1] -= c; d[x2 + 1][y1] -= c; d[x2 + 1][y2 + 1] += c; } }
前缀和数组(二维)
“二维前缀和” 模板:
S[i, j] = 第i行j列格子左上部分所有元素的和 S[i, j] = S[i-1,j] + s[i,j-1] - S[i-1,j-1] + a[i,j](表示当前的数)(双重for循环构建) 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: S[x2, y2] - S[x1-1,y2] - S[x2,y1-1] + S[x1-1,y1-1] int matrix[M][N], prefix[M][N]; // M、N分别表示二维矩阵的高度和宽度 void CreatePrefix(int m, int n) { for(int i=1;i<=m;i++) // 二维矩阵的录入以及二维前缀和数组的构建 for(int j=1;j<=n;j++){ cin>>matrix[i][j]; prefix[i][j] = prefix[i-1][j]+ prefix[i][j-1]- prefix[i-1][j-1]+matrix[i][j]; } } //在给定了两个序数对(x1, y1)、(x2, y2)后, //通过二维前缀和数组求解指定子阵元素和的公式为: S=prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 -1] + prefix[x1 -1][y1 -1]
一维前缀和的主要用处是对一维数组的子区间进行快速求和,因此在面对矩阵时便失去了其作用。比如对于如下二维表:
若现在有w组询问,每组询问给出两个序数对(x1, y1),(x2, y2)(两个序数对满足x1<x2, y1<y2),对于每组询问,你需要输出以这两个序数对所确定的矩阵中的所有元素之和。
在这样的需求下,二维前缀和“千呼万唤始出来”。二维前缀和prefix[i][j]的数理定义是:
它表示从位置matrix[1][1]到matrix[i][j]这之间所有元素的总和。下面我们来关注一下如何通过一个二维数组来构建二维前缀和(这就需要我们寻找二维前缀和的迭代公式)。
如下图所示,prefix[2][3]表示图中黄色部分的所有元素之和。
由该图易知,prefix[2][3] = matrix[1][1] + matrix[1][2] + matrix[1][3] + matrix[2][1] + matrix[2][2] + matrix[2][3] = prefix[2][2] + matrix[1][3] + matrix[2][3]。
如下图所示,prefix[3][2]表示图中蓝色部分的所有元素之和。
由该图易知,prefix[3][2] = matrix[1][1] + matrix[1][2] + matrix[2][1] + matrix[2][2] + matrix[3][1] + matrix[3][2] = prefix[2][2] + matrix[3][1] + matrix[3][2]。
由于prefix [2][3] + prefix [3][2] = 2 * prefix [2][2] + matrix[1][3] + matrix[2][3] + matrix[3][1] + matrix[3][2]。也就是说将这两幅图进行重叠得到的效果如图3.1.4所示(其中绿色部分,即prefix [2][2]占了两份):
那么如果我们要用原数组和前缀和数组得到prefix[3][3]的话,则公式为:
prefix[3][3] = prefix[2][3] + prefix[3][2] - pfofix[2][2] + matrix[3][3]实际上,上式正是二维前缀数组递推式的一个实例。根据斥容原理,我们也不难得出二维前缀数组的递推式为:
prefix[i][j] = prefix [i-1][j] + prefix [i][j-1] - prefix [i-1][j-1] + matrix[i][j]
由该式,我们可以直接写出构建二维前缀数组的代码:
int matrix[M][N], prefix[M][N]; // M、N分别表示二维矩阵的高度和宽度 void CreatePrefix(int m, int n) { for(int i=1;i<=m;i++) // 二维矩阵的录入以及二维前缀和数组的构建 for(int j=1;j<=n;j++){ cin>>matrix[i][j]; prefix[i][j] = prefix[i-1][j]+ prefix[i][j-1]- prefix[i-1][j-1]+matrix[i][j]; } }
接下来我们来讨论如何利用二位前缀数组来求解最初的问题。
假设现在给出两个序数对(2, 2)和(4, 4)(下图中红色部分),我们要怎么利用二维前缀数组来求出这两点所确定的子矩阵元素之和呢(图中红色与黄色的共同组成部分)?
如果仅仅是用prefix[4][4] - prefix [2][2],得到的结果如下图中黄色部分所示:
显然这个结果并不是我们所预想的。此时我们可以从二维前缀和的定义式中寻找突破。如果我们将所求子阵单独隔离出去,来观察剩余元素的位置特征,如下图所示(有色背景部分):
若设图中黄色部分的子阵为S1,蓝色部分的子阵为S2,绿色部分的子阵为S3(=prefix[1][1]),待求子阵为S。那么我们可以很容易地得到:
S = prefix[4][4] - ( S1 + S2 + S3 ) = prefix[4][4] - ( (S1+S3) + (S2+S3) - S3 ) = prefix[4][4] - ( prefix[1][4] + prefix[4][1] - prefix[1][1] ) = prefix[4][4] - prefix[1][4] - prefix[4][1] + prefix[1][1]
实际上,在给定了两个序数对(x1, y1)、(x2, y2)后,通过二维前缀和数组求解指定子阵元素和的公式为:
S = prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 -1] + prefix[x1 -1][y1 -1]
前缀和的应用
【洛谷】 P1115 最大子段和
public class demo79_前缀和_最大子段和 { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); //sum用于记录前缀和,max用于记录最大值,now用于记录当前输入的值 int n, sum, max, now; n = scanner.nextInt(); //将输入序列的第一个值作为sum scanner.nextLine(); //将sum的值作为max的初始值 sum = scanner.nextInt(); max = sum; while (--n > 0) { now = scanner.nextInt(); sum = Math.max(sum, 0); //判断sum的正负以决定是否保留该序列 sum += now; //无论怎样,都加上当前输入的值以对比max max = Math.max(max,sum); } System.out.println(max); } }
public class demo79_前缀和数组_子矩阵的和 { static int N = 1005; static int n, m, q; static int[][] map = new int[N][N]; static int[][] prefix = new int[N][N]; public static void main(String[] args) { Scanner scanner = new Scanner(System.in); n = scanner.nextInt(); m = scanner.nextInt(); q = scanner.nextInt(); scanner.nextLine(); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { map[i][j] = scanner.nextInt(); prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1] + map[i][j]; } } int x1, y1, x2, y2; while (q-- > 0) { x1 = scanner.nextInt(); y1 = scanner.nextInt(); x2 = scanner.nextInt(); y2 = scanner.nextInt(); System.out.println(prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 - 1] + prefix[x1 - 1][y1 - 1]); } } }
递归
递归的含义很好理解,就是一个函数调用自身,难就难在如何确定一个题目的递归式,这就需要多刷题了。 一个完整的递归函数包含两个部分:
- 递归式
- 递归出口
以斐波那契数列为例:
int f(int n){ if(n == 1 || n == 2) return 1; // 递归出口 return f(n-1) + f(n-2); // 递归式 }
递归式用来递归计算我们想要得到的值, 递归出口用来结束递归。
如果没有递归出口,那么就会一直递归下去,就造成了死循环。
那么什么题会用到递归呢? 子问题和原问题求解方式完全相同的,可以用递归。
DFS
八皇后问题
public class Queue8Simple { static int max = 8;//定义一个max表示共有多少个皇后 //定义数组array, 保存皇后放置位置的结果,比如 //arr = {0 , 4, 7, 5, 2, 6, 1, 3} ----数字表示列 static int[] array = new int[max]; static int count = 0;//解法数目 public static void main(String[] args) { dfs(0); System.out.printf("一共有%d解法", count); } //编写一个方法,放置第n个皇后 //特别注意: dfs 是 每一次递归时,进入到dfs中都有 //for(int i = 0; i < max; i++),因此会有回溯 private static void dfs(int n) { if (n == max) { //n = 8 , 其实8个皇后就既然放好 return; } //依次放入皇后每一列(i),并判断是否冲突 //如果冲突,就继续循环,看下一列是否能成功放置皇后 for (int i = 0; i < max; i++) { //i的值代表皇后在第几列 array[n] = i; //判断当放置第n个皇后到i列时,是否冲突 for (int j = 0; j < n; j++) { if (!(array[j] == array[n] || Math.abs(n - j) == Math.abs(array[n] - array[j]))) { dfs(n + 1); } } } } }
迷宫问题
题目大意就是从起始位置@开始走,只能走在 . (黑砖)上面, 不能走在#(红砖)上面,问从@开始最多可以走几块 .(黑砖)。
将每个可以到达的点当作一个状态,搜索所有的状态,就可以得到答案啦。
怎么搜索呢, 只要我们得到下个点的坐标就可以了, 可以朝四个方向走,
当前坐标 + (0,1) 就是向右走, + (1,0)就是向下走, + (0,-1)就是向左走, +(-1, 0)就是向上走。
判断一下个点是否超出边界是红砖还是黑砖,如果是黑砖,就搜索下一点,也就是DFS下一个点。
本题也可BFS。
本题坑点: @点也算一个答案。
import java.io.*; import java.util.Arrays; public class Main{ static BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); static char[][] map = new char[30][30]; static int[][] vis = new int[30][30]; static int[][] ne = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; static int n, m, res = 0; public static void dfs(int x, int y){ vis[x][y] = 1; for(int i = 0; i < 4; i++){ //四个方向 int nx = x + ne[i][0]; int ny = y + ne[i][1]; if(nx < 0 || nx >= m || ny < 0 || ny >= n || map[nx][ny] == '#' || vis[nx][ny] == 1){ continue; } res++; dfs(nx,ny); } } public static void main(String[] args) throws IOException{ while(true){ String s[] = in.readLine().split(" "); n = Integer.parseInt(s[0]); m = Integer.parseInt(s[1]); if(n==0||m==0) break; int x = 0, y = 0; res = 0; for(int i = 0; i < vis.length; i++) Arrays.fill(vis[i], 0); for(int i = 0; i < m; i++){ String s1 = in.readLine(); if(s1.indexOf('@') != -1){ x = i; y = s1.indexOf('@'); } map[i] = s1.toCharArray(); } dfs(x, y); out.write((res+1)+"\n"); out.flush(); } } }
马踏棋盘(贪心优化)
这里值得一提的有两个地方:
①用到了awt包中的Point类,类似于平时自己构建的Node,具体方法查看源码
②运用贪心算法的思想,每次选择可走路线最少的路去走,最大程度减少回溯
import java.awt.Point; import java.util.ArrayList; import java.util.Comparator; public class HorseChessboard { private static int X; // 棋盘的列数 private static int Y; // 棋盘的行数 //创建一个数组,标记棋盘的各个位置是否被访问过 private static boolean[] visited; //使用一个属性,标记是否棋盘的所有位置都被访问 private static boolean finished; // 如果为true,表示成功 public static void main(String[] args) { System.out.println("骑士周游算法,开始运行~~"); //测试骑士周游算法是否正确 X = 8; Y = 8; int row = 1; //马儿初始位置的行,从1开始编号 int column = 1; //马儿初始位置的列,从1开始编号 //创建棋盘 int[][] chessboard = new int[X][Y]; visited = new boolean[X * Y];//初始值都是false //测试一下耗时 long start = System.currentTimeMillis(); dfs(chessboard, row - 1, column - 1, 1); long end = System.currentTimeMillis(); System.out.println("共耗时: " + (end - start) + " 毫秒"); //输出棋盘的最后情况 for(int[] rows : chessboard) { for(int step: rows) { System.out.print(step + "\t"); } System.out.println(); } } /** * 完成骑士周游问题的算法 * @param chessboard 棋盘 * @param row 马儿当前的位置的行 从0开始 * @param column 马儿当前的位置的列 从0开始 * @param step 是第几步 ,初始位置就是第1步 */ public static void dfs(int[][] chessboard, int row, int column, int step) { chessboard[row][column] = step; //这里是把二维数组降维为一维数组,通过加上前面的行数和当前的列数,得到现在是第几个元素 visited[row * X + column] = true; //标记该位置已经访问 //获取当前位置可以走的下一个位置的集合 //-----------不能把这个list写在外面!每次递归都需要一个单独的list对象,表示每层递归中下一次可以走的点的集合 ArrayList<Point> ps = next(new Point(column, row)); //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序 //-----------下面这句话是贪心的体现,并且要注意: // ---------------不要直接在这里面写比较器,因为每次创建并且执行这样的操作,也会花费时间,单独写一个方法来比较,用空间换时间 sort(ps); //遍历 ps while(!ps.isEmpty()) { Point p = ps.remove(0);//取出下一个可以走的位置 //判断该点是否已经访问过 //-----------这里把x 看成列,y 看成行,模拟坐标中的(x,y) if(!visited[p.y * X + p.x]) {//说明还没有访问过 dfs(chessboard, p.y, p.x, step + 1); } } //判断马儿是否完成了任务,使用 step 和应该走的步数比较 , //如果没有达到数量,则表示没有完成任务,将整个棋盘置0 //说明: step < X * Y 成立的情况有两种 //1. 棋盘到目前位置,仍然没有走完 //2. 棋盘处于一个回溯过程 //------这里是回溯的过程,从dfs的最深处开始回溯,如果没有走完,把状态重置,以免影响下一次的递归 if(step < X * Y && !finished ) { chessboard[row][column] = 0; visited[row * X + column] = false; } else { finished = true; } } /** * 功能: 根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList), 最多有8个位置 * @param curPoint * @return */ public static ArrayList<Point> next(Point curPoint) { //创建一个ArrayList ArrayList<Point> ps = new ArrayList<>(); //创建一个Point Point p1 = new Point(); //表示马儿可以走5这个位置 if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) { ps.add(new Point(p1)); } //判断马儿可以走6这个位置 if((p1.x = curPoint.x - 1) >=0 && (p1.y=curPoint.y-2)>=0) { ps.add(new Point(p1)); } //判断马儿可以走7这个位置 if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) { ps.add(new Point(p1)); } //判断马儿可以走0这个位置 if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) { ps.add(new Point(p1)); } //判断马儿可以走1这个位置 if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) { ps.add(new Point(p1)); } //判断马儿可以走2这个位置 if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) { ps.add(new Point(p1)); } //判断马儿可以走3这个位置 if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) { ps.add(new Point(p1)); } //判断马儿可以走4这个位置 if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) { ps.add(new Point(p1)); } return ps; } //根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数 public static void sort(ArrayList<Point> ps) { ps.sort(new Comparator<Point>() { @Override public int compare(Point o1, Point o2) { //获取到o1的下一步的所有位置个数 int count1 = next(o1).size(); //获取到o2的下一步的所有位置个数 int count2 = next(o2).size(); if(count1 < count2) { return -1; } else if (count1 == count2) { return 0; } else { return 1; } } }); } }
BFS
BFS要先搜索当前状态可以直接到达的所有状态。因为一次只能处理一个状态,所以我们需要按照先后顺序,先将可以到达的所有状态点全部存起来,因为需要满足先后的顺序,所以正好可以利用队列先进先出的特性来存储。
static int dir[][] = {{0, -1}, {1, 0}, {-1, 0}, {0, 1}};//路径有时候很重要
模板
public static void bfs(int x, int y){ q.add(x, y); //将起点入队 vis[x][y] = 1; // 标记已经走过 while(!q.isEmpty()){ // 当队列不空 x = q.peek().x; //取出队头元素 y = q.peek().y; q.poll(); //删除队头元素 if(x == n-1 && n == m-1) return; // 到达终点 for(int i = 0; i < 4; i++){ // 当前点可以到达的下四个方向 int nx = x + dx[i]; // 下个点的坐标 int ny = y + dy[i]; if(nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny] == 1 ) continue; // 下个点不合法 else{ q.add(new pair(nx,ny)); //下个点合法,入队存储 vis[nx][ny] = 1; // 标记该点已经走过 } } } }
走迷宫
给定一个n’m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1,1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1,1)处和(n, m)处的数字为0,且一定至少存在一条通路。输入格式
第一行包含两个整数n和m。
接下来n行,每行包含m个整数(O或1),表示完整的二维数组迷宫。输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
import java.io.*; import java.util.*; class pair{ public int x,y; pair(int a, int b){ x = a; y = b; } } public class Main{ static BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); static final int N = 1000; static String[][] arr = new String[N][N]; static int[][] pren = new int[N][N]; static int[][] vis = new int[N][N]; static int[][] res = new int[N][N]; static int[] dx = {-1, 0, 1, 0}; static int[] dy = {0, 1, 0, -1}; static int n, m = 0; static Queue<pair> q = new LinkedList<>(); public static int Int(String s){ return Integer.parseInt(s);} public static void read() throws IOException{ for(int i = 0; i < n; i++){ arr[i] = in.readLine().split(" "); } } public static void bfs(int x, int y)throws Exception{ q.add(new pair(x, y)); vis[x][y] = 1; res[x][y] = 0; while(!q.isEmpty()){ x = q.peek().x; y = q.peek().y; q.poll(); for(int i = 0; i < 4; i++){ int nx = x + dx[i]; int ny = y + dy[i]; if(nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny] == 1 || arr[nx][ny].compareTo("1") == 0) continue; else{ res[nx][ny] = res[x][y] + 1; q.add(new pair(nx,ny)); vis[nx][ny] = 1; if(nx == n-1 && ny == m-1) return; } } } } public static void main(String[] args) throws Exception{ String s[] = in.readLine().split(" "); n = Int(s[0]); m = Int(s[1]); read(); bfs(0,0); out.write(res[n-1][m-1]+"\n"); out.flush(); } }