前缀和和差分以及练习题目

蓝桥杯备赛系列

倒计时50天!

前缀和和差分

知识点

前缀和数组:
假设原数组用a[i]表示,前缀和数组用sum[i]表示,那么sum[i]表示的是原数组前i项之和,注意一般用前缀和数组时,原数组a[i]的有效下标是从1开始的。式子如下,
s u m [ n ] = ∑ i = 1 n a [ i ] sum[n]=\sum_{i=1}^n a[i] sum[n]=i=1na[i]
差分数组:
假设原数组用a[i]表示,差分数组用d[i]表示,那么d[i]表示的是a[i]和a[i-1]之差,注意一般用差分数组时,原数组a[i]的有效下标也是从1开始的。式子如下,
d [ n ] = a [ i ] − a [ i − 1 ] d[n]=a[i]-a[i-1] d[n]=a[i]a[i1]
接下来看一下前缀和和差分的模板题。

前缀和模板

题目描述

给定一个长度为 n n n的数组 a 1 , a 2 , . . . . a n . a_1,a_2,....a_n. a1,a2,....an.

接下来有 q q q次查询, 每次查询有两个参数 l , r l, r l,r.

对于每个询问, 请输出 a l + a l + 1 + . . . + a r . a_l+a_{l+1}+...+a_r. al+al+1+...+ar.

输入描述

第一行包含两个整数 n n n q q q.

第二行包含n个整数, 表示 a 1 , a 2 , . . . . a n . a_1,a_2,....a_n. a1,a2,....an.

接下来q行,每行包含两个整数 l和r.

1 ≤ n , q ≤ 10 5 1≤n,q≤10^5 1n,q105
− 10 9 ≤ a [ i ] ≤ 10 9 −10^9≤a[i]≤10^9 109a[i]109
1 ≤ l ≤ r ≤ n 1≤l≤r≤n 1lrn

输出描述

输出 q q q行,每行代表一次查询的结果.

样例输入

3 2
1 2 4
1 2
2 3

样例输出

3
6
题目分析

前缀和最经典的一个作用就是以O(1)的时间复杂度求区间和。

sum[l-1]=a[1]+a[2]+a[3]+…+a[l-1]

sum[r]=a[1]+a[2]+a[3]+…+a[l-1]+a[l]+…+a[r]

sum[r]-sum[l-1]=a[1]+a[2]+a[3]+…+a[l-1]+a[l]+…+a[r]-(a[1]+a[2]+a[3]+…+a[l-1])

​ =a[l]+a[l+1]+…+a[r]

所以区间[l,r]的和可以用sum[r]-sum[l-1]表示。

题目代码
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int q = scanner.nextInt();
	int a[] = new int[n+1];
	long sum[] = new long[n+1];
	for(int i = 1;i <= n;i++) a[i] = scanner.nextInt();
	for(int i = 1;i <= n;i++) sum[i] = sum[i-1] + a[i];
	while(q-- > 0) {
		int l = scanner.nextInt();
		int r = scanner.nextInt();
		System.out.println(sum[r]-sum[l-1]);
	}
}
}
二维前缀和模板

题目描述

给你一个 n 行 m 列的矩阵 A ,下标从1开始。

接下来有 q 次查询,每次查询输入 4 个参数 x 1 , y 1 , x 2 , y 2 x1 , y1 , x2 , y2 x1,y1,x2,y2

请输出以 ( x 1 , y 1 ) (x1, y1) (x1,y1)为左上角 ,$ (x2,y2)$ 为右下角的子矩阵的和,

输入描述

第一行包含三个整数n,m,q.

接下来n行,每行m个整数,代表矩阵的元素

接下来q行,每行4个整数 x 1 , y 1 , x 2 , y 2 x1, y1, x2, y2 x1,y1,x2,y2,分别代表这次查询的参数

1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000
1 ≤ q ≤ 10 5 1≤q≤10^5 1q105
− 10 9 ≤ a [ i ] [ j ] ≤ 10 9 −10^9≤a[i][j]≤10^9 109a[i][j]109
1 ≤ x 1 ≤ x 2 ≤ n 1≤x1≤x2≤n 1x1x2n
1 ≤ y 1 ≤ y 2 ≤ m 1≤y1≤y2≤m 1y1y2m

输出描述

输出q行,每行表示查询结果。

样例输入

3 4 3
1 2 3 4
3 2 1 0
1 5 7 8
1 1 2 2
1 1 3 3
1 2 3 4

样例输出

8
25
32
题目分析

首先要了解二维前缀和数组的定义, s u m [ i ] [ j ] sum[i][j] sum[i][j]表示的是对左上角行列分别为1,1,右下角行列分别为i,j的矩阵求和。 s u m [ 7 ] [ 7 ] sum[7][7] sum[7][7]如下图,为红色圈起来的矩阵。注意这里矩阵的下标都是从1开始的。

在这里插入图片描述

我们通过图片分析如何求二维前缀和数组以及如何利用二维前缀和数组求矩阵和。

如何求二维前缀和数组。如图所示,假设要求 s u m [ x 1 ] [ y 1 ] sum[x1][y1] sum[x1][y1]的值,也就是图中大红色圈起来的部分,我们可以将蓝色部分加上绿色部分后再加上x1,y1处本来的值,也就是 s u m [ x 1 ] [ y 1 − 1 ] + s u m [ x 1 − 1 ] [ y 1 ] + a [ x 1 ] [ y 1 ] sum[x1][y1-1]+sum[x1-1][y1]+a[x1][y1] sum[x1][y11]+sum[x11][y1]+a[x1][y1]。但是这样会多加上一部分,也就是蓝色和绿色重叠的部分,他用 s u m [ x 1 − 1 ] [ y 1 − 1 ] sum[x1-1][y1-1] sum[x11][y11]表示,需要减掉。综上 s u m [ x 1 ] [ y 1 ] = s u m [ x 1 ] [ y 1 − 1 ] + s u m [ x 1 − 1 ] [ y 1 ] + a [ x 1 ] [ y 1 ] − s u m [ x 1 − 1 ] [ y 1 − 1 ] sum[x1][y1]=sum[x1][y1-1]+sum[x1-1][y1]+a[x1][y1]-sum[x1-1][y1-1] sum[x1][y1]=sum[x1][y11]+sum[x11][y1]+a[x1][y1]sum[x11][y11]

如何利用二维前缀和数组求矩阵和。如图所示,假设要求左上角下标为x1,y1,右下角下标为x2,y2的矩阵的值,也就是图中蓝色的部分。可以用 s u m [ x 2 ] [ y 2 ] − s u m [ x 2 ] [ y 1 − 1 ] − s u m [ x 1 − 1 ] [ y 2 ] + s u m [ x 1 − 1 ] [ y 1 − 1 ] sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1] sum[x2][y2]sum[x2][y11]sum[x11][y2]+sum[x11][y11]表示,也就是图中绿色部分减去图中橙色和紫色部分,而紫色和橙色部分有重叠,多减了,要再加回来,也就是图中棕色部分。

题目代码

代码细节,数据的读入涉及到矩阵,对于java而言数据量较大,要使用快读。如果没有学过快读,可以点击链接学习一下。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
public static void main(String[] args) throws IOException {
	BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	String[] strings = br.readLine().split(" ");
	int n = Integer.parseInt(strings[0]);
	int m = Integer.parseInt(strings[1]);
	int q = Integer.parseInt(strings[2]);
	long a[][] = new long[n+1][m+1];
	long sum[][] = new long[n+1][m+1];
	for(int i = 1;i <= n;i++) {
		strings = br.readLine().split(" ");
		for(int j = 1;j <= m;j++) {
			a[i][j] = Integer.parseInt(strings[j-1]);
		}
	}
	
	for(int i = 1;i <= n;i++) {
		for(int j = 1;j <= m;j++) {
			sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];
		}
	}
	while(q-- > 0) {
		strings = br.readLine().split(" ");
		int x1 = Integer.parseInt(strings[0]);
		int y1 = Integer.parseInt(strings[1]);
		int x2 = Integer.parseInt(strings[2]);
		int y2 = Integer.parseInt(strings[3]);
		System.out.println(sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]);
	}
}
}

差分模板题

题目描述

给你一个长度为n的正数数组 a 1 , a 2 , . . . . a n a_1,a_2,....a_n a1,a2,....an

接下来对这个数组进行m次操作,每个操作包含三个参数l,r,k,代表将数组中 a l + a l + 1 + . . . + a r a_l+a_{l+1}+...+a_r al+al+1+...+ar部分都加上k。

请输出操作后的数组。

输入描述

第一行包含两个整数n和m。
第二行包含n个整数表示 a 1 , a 2 , . . . . a n a_1,a_2,....a_n a1,a2,....an
接下来是m行,每行三个整数,分别代表每次操作的参数l,r,k.

1 ≤ n , m ≤ 10 5 1≤n,m≤10^5 1n,m105
− 10 9 ≤ a [ i ] ≤ 10 9 −10^9≤a[i]≤10^9 109a[i]109
1 ≤ l ≤ r ≤ n 1≤l≤r≤n 1lrn
− 10 9 ≤ k ≤ 10 9 −10^9≤k≤10^9 109k109

输出描述

输出1行,表示m次操作后的 a 1 , a 2 , . . . . a n a_1,a_2,....a_n a1,a2,....an

输入样例

3 2
1 2 3
1 2 4
3 3 -2

输出样例

5 6 1
题目分析

差分数组一般要结合前缀和使用,使用方法如下,

假设我有两个操作,第一个操作是对下标为3一直到下标为10的数字都加上2,第二个操作是对下标为4到下标为7的数字都加上3,朴素的做法是遍历一遍要操作的数组区间,然后执行操作就可以了,两次操作过后,原始数组累计要改变的值如下,

a的下标1234567891011
a要加上的值00255552220

这样做执行每次操作的时间复杂度是数组的长度。那么如何利用差分数组来做呢?

对下标为3一直到下标为10的数字都加上2——>d[3]+=2,d[11]-=2。

对下标为4到下标为7的数字都加上3——>d[4]+=3,d[8]-=3。

然后求d数组的前缀和数组,结果如下,下标从1开始

sum的下标1234567891011
sum的值00255552220

此时sum里面的值表示的是原始数组要变动的值,将sum数组与原始数组相加,即是操作后的结果,可以发现是和表1一样的。而这样每次操作的时间复杂度是O(1)。

综上,如果我要对区间[l,r]里的数加上k,我只需要对差分数组进行操作,即d[l]+=k,d[r+1]-=k。

题目代码
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int m = scanner.nextInt();
	long[] a = new long[n+1];
	long[] s = new long[n+1];
	for (int i = 1; i < a.length; i++) {
		a[i] = scanner.nextInt();
	}
	while(m>0) {
		m--;
		int l = scanner.nextInt();
		int r = scanner.nextInt();
		int k = scanner.nextInt();
	    s[l] += k;
	    if(r+1<=n) {
	    	s[r+1]-=k;
	    }
	}
	long sum[] = new long[n+1];
	for (int i = 1; i < s.length; i++) {
		sum[i] = sum[i-1]+s[i];
		a[i] = a[i]+sum[i];
		System.out.print(a[i] +" ");
	}
}
}
二维差分模板题

题目描述

给你一个n行m列的矩阵,下标从1开始。
接下来有q次操作,每次操作输入5个参数x1, y1, x2, y2, k
表示把以(x1, y1)为左上角,(x2,y2)为右下角的子矩阵的每个元素都加上k,
请输出操作后的矩阵。

输入描述

第一行包含三个整数n,m,q.
接下来n行,每行m个整数,代表矩阵的元素

接下来q行,每行5个整数x1, y1, x2, y2, k,分别代表这次操作的参数

1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000
1 ≤ q ≤ 10 5 1≤q≤10^5 1q105
1 ≤ x 1 ≤ x 2 ≤ n 1≤x1≤x2≤n 1x1x2n
1 ≤ y 1 ≤ y 2 ≤ m 1≤y1≤y2≤m 1y1y2m
− 10 9 ≤ 矩阵中的元素 ≤ 10 9 −10^9≤矩阵中的元素≤10^9 109矩阵中的元素109

输出描述

输出n行,每行m个数,每个数用空格分开,表示这个矩阵。

样例输入

2 3 4
1 2 3
4 5 6
1 1 2 2 3
1 2 2 3 -1
1 1 1 3 4
1 1 2 1 1

样例输出

9 8 6
8 7 5
题目分析

二维差分模板关键在于确定好在差分数组的哪些地方更改值。假设要更改的地方是从下标(2,4)到下标(3,6)的位置加1,那么首先必然是让(2,4)的位置加1,这样之后对差分数组求前缀和的结果如图所示,

我们要更改的是绿色圈起来的部分,但是之外的部分也更改了,我需要把这部分更改变回来,我想让(2,6)之后的从(2,7)开始都是0,既然(2,4)加了1,那么对应的(2,7)这一部分之后我要减掉1,来抵消前面的加1。同样,(4,4)也要减1来抵消(2,4)的加1。这样就可以了吗?这样相当于有两个位置减1,一个位置加1,一看也不对呀,对于(4,7)这个位置应该是没有变化的,但是它的前缀和所包含的格子里(2,4)加了1,(2,7)减了1,(4,4)也减了1,相当于(4,7)这个位置减了1,我们要把这个减1抵消,所以要在(4,7)这里加1。

综上,对(x1,y2)到(x2,y2)的格子都进行加k操作,只需要(x1,y1)+=k,(x1,y2+1)-=k,(x2+1,y1)-=k,(x2+1,y2+1)+=k。

然后对差分数组求二维前缀和之后,把结果累加到原始数组上就可以了。

题目代码
import java.util.Scanner;
public class Main{
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int m = scanner.nextInt();
		int q = scanner.nextInt();
		long[][] a = new long[n + 1][m + 1];
		long[][] b = new long[n + 1][m + 1];
		long[][] sum = new long[n + 1][m + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++) {
				a[i][j] = scanner.nextLong();
			}
		}
		while ((q--) != 0) {
			int x1 = scanner.nextInt();
			int y1 = scanner.nextInt();
			int x2 = scanner.nextInt();
			int y2 = scanner.nextInt();
			int k = scanner.nextInt();
			b[x1][y1] += k;
			if (y2 + 1 <= m) {
				b[x1][y2 + 1] -= k;
			}
			if (x2 + 1 <= n) {
				b[x2 + 1][y1] -= k;
			}
			if (x2 + 1 <= n && y2 + 1 <= m) {
				b[x2 + 1][y2 + 1] += k;
			}
		}

		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++) {
				sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+b[i][j];
				a[i][j] += sum[i][j];
				System.out.print(a[i][j] + " ");
			}
			System.out.println();
		}

	}
}
abb

题目描述

leafee 最近爱上了 abb 型语句,比如“叠词词”、“恶心心”

leafee 拿到了一个只含有小写字母的字符串,她想知道有多少个 “abb” 型的子序列?
定义: abb 型字符串满足以下条件:

  1. 字符串长度为 3 。
  2. 字符串后两位相同。
  3. 字符串前两位不同。

输入描述

第一行一个正整数 n

第二行一个长度为 n 的字符串(只包含小写字母)

1 ≤ n ≤ 10 5 1≤n≤10^5 1n105

输出描述

“abb” 型的子序列个数。

样例输入

6
abcbcc

样例输出

8

共有1个abb,3个acc,4个bcc

题目分析

在abcbcc中,acc的个数为3,他是哪3个呢?可以看下图,a的后面有3个c,在这3个c里面任选两个c可以和a组成一个叠词,那么组合的种类为 C 3 2 C_3^2 C32也就是 3 ∗ 2 / 2 = 3 3*2/2=3 32/2=3种。假设字母a后面有num个相同的字母,那么它能够贡献的叠词个数为 C n u m 2 C_{num}^2 Cnum2,也就是 n u m ∗ ( n u m − 1 ) / 2 num*(num-1)/2 num(num1)/2个。因此我们需要求出每个字母后面出现的其它相同字母的个数。

在这里插入图片描述

对于上图字符串,因为求的是后面的字符出现的次数,所以我们倒序遍历去求。位置6c出现了一次,用 n u m [ 6 ] [ c ] = 1 num[6][c]=1 num[6][c]=1表示,位置5c出现了1次,用 n u m [ 5 ] [ c ] = 1 num[5][c]=1 num[5][c]=1表示,位置3c出现了一次,用 n u m [ 3 ] [ c ] = 1 num[3][c]=1 num[3][c]=1表示,其余位置 n u m [ 1 , 2 , 4 ] [ c ] = 1 num[1,2,4][c]=1 num[1,2,4][c]=1都是0。那么求位置4后面包括位置4在内c出现的次数,应该是位置6的c累加上位置5的c,即 n u m [ 5 ] [ c ] + n u m [ 6 ] [ c ] num[5][c]+num[6][c] num[5][c]+num[6][c],求位置1后面c出现的次数,应该是 n u m [ 2 ] [ c ] + n u m [ 3 ] [ c ] + n u m [ 4 ] [ c ] + n u m [ 5 ] [ c ] + n u m [ 6 ] [ c ] = 0 + 1 + 0 + 1 + 1 = 3 num[2][c]+num[3][c]+num[4][c]+num[5][c]+num[6][c]=0+1+0+1+1=3 num[2][c]+num[3][c]+num[4][c]+num[5][c]+num[6][c]=0+1+0+1+1=3。这里其实就是前缀和数组或者说是后缀和数组,因为求的是后面的值累加的结果。

综上,我们用 n u m s [ i ] [ c ] nums[i][c] nums[i][c]表示下标为i的位置的右边字符c出现的总次数。这里的数组就是后缀和数组。即 n u m s [ i ] [ c ] = n u m [ i + 1 ] [ c ] + n u m [ i + 2 ] [ c ] + . . . + n u m [ n ] [ c ] nums[i][c]=num[i+1][c]+num[i+2][c]+...+num[n][c] nums[i][c]=num[i+1][c]+num[i+2][c]+...+num[n][c]

那么 n u m s [ i ] [ c ] = n u m s [ i + 1 ] [ c ] + n u m [ i ] [ c ] nums[i][c]=nums[i+1][c]+num[i][c] nums[i][c]=nums[i+1][c]+num[i][c]

题目代码
import java.util.Scanner;
public class abb {
public static void main(String[] args) {
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	char[] s = (" "+scanner.next()).toCharArray();
	int pre[][] = new int[n+2][26];
	for(int i = n;i >= 1;i--) {
		for(int j = 0;j < 26;j++) {
            //求下标为i的位置包括下标i在内26个字母中,每个字母出现的次数。这里要用到前缀和
			pre[i][j] =pre[i+1][j];
		}
		pre[i][s[i]-'a']++;//这里就是加上位置i的字母
	}
	long sum = 0;
	for(int i = 1;i <= n;i++) {
		for(int j = 0;j < 26;j++) {
            //注意这里要有j!=s[i]-'a'的判断,因为aaa是不合法的
            //同样也要有pre[i][j]>=2的判断,因为后面至少要有两个才能组合出acc
            //pre[i+1][j]是i+1,因为pre[i][j]包含了位置i的字母,但是我们求的是i后面的字母
            //但其实用pre[i][j]也没有问题,因为这里的字母j肯定和位置i的字母不一样,那么必然不会对pre[i][j]数组产生作用,因为这里的num[i][j]=0
			if(pre[i][j]>=2&&j!=s[i]-'a') sum += (pre[i+1][j]-1)*pre[i
+1][j]/2;
		}
	}
	System.out.println(sum);
}
}
鼠鼠我鸭

题目描述

在一个叫做酱西功爷枝叶鸡树学院的地方有n只小动物,要么是鼠鼠,要么是鸭鸭,从1到n编号,每只小动物有个体重 a i a_i ai

在这个学校里,存在一种神奇的魔法,可以将编号位于某个区间 [ l , r ] [l,r] [l,r]内的所有鼠鼠都变为鸭鸭,鸭鸭都变为鼠鼠(魔法并不会改变体重)。

现在你可以施放这个魔法至多1次。(也可以不施放)

问最终鸭鸭的总重量最多是多少?

输入格式

第一行一个整数T表示样例个数。 ( 1 ≤ T ≤ 10 ) (1≤T≤10) (1T10)

对于每个样例:

第一行一个整数n表示小动物的个数。( 1 ≤ n ≤ 10 5 1≤n≤10^5 1n105)

第二行 n n n个整数,表示第 i i i个小动物的类型。0表示鼠鼠,1表示鸭鸭。

第三行 n n n个整数,表示第 i i i个小动物的体重 a i a_i ai。( 1 ≤ a i ≤ 10 9 1≤a_i≤10^9 1ai109)

输出格式

对于每个样例一行一个整数表示答案。

样例输入

2
3
0 0 0
1 2 3
4
0 1 0 0
2 5 6 5

样例输出

6
16
题目分析

假设原本所有鸭鸭的重量是sum,操作后形成的偏移是fix,即经过至多1次操作后鸭鸭的总重量是sum+fix,这里的fix最小是0,因为如果操作后的fix是负数,会使得鸭鸭的总重量减少,倒不如不操作,直接是sum就可以了。

来看这个fix应该怎么求。以样例的第二组数据为例,如果操作区间是[0,4],那么每个位置对应的偏移量分别为2,-5,6,5。总的偏移量为2-5+6+5=8。如果操作区间是[1,2],总的偏移量为-5+6=1。其实要求某个区间的偏移量就是数组[2,-5,6,5]对应区间的区间和。这里的[2,-5,6,5]也叫做偏移量数组,因为对于位置i的值而言,如果他是鸭鸭,一次操作后就变为了鼠鼠,那么之前累加上的他的重量应该减去,所以偏移量就是负的该位置的重量也就是-a[i],如果他是鼠鼠,一次操作后就变为了鸭鸭,他的重量应该累加上,所以偏移量就是该位置的重量也就是a[i]。所以我们可以求一下偏移量数组的前缀和,然后利用前缀和求最大的偏移量区间和,也就是一开始说的fix,累加到原始的重量和sum里面就好。

假设偏移量的前缀和数组是pre[i],那么偏移量的区间和[l,r]就用pre[r]-pre[l-1],如果我们遍历l和r的话,双层嵌套for循环会超时,所以这里应该采用类似双指针的做法,假设固定了r,要想pre[r]-pre[l-1]最大,需要pre[l-1]是区间[1,l-1]里面最小的那个数,用mi表示,关键代码如下,

fix=0;mi=0;
for(int i=1;i<=n;i++)  
		{
			fix=max(fix,s[i]-mi);//i固定时,mi是下标比i小的数中的最小值
			mi=min(mi,s[i]);
		}

fix最小是0,所以初始化为0。这里的mi最小值也为0,mi为0时表示的是区间[1,i]的区间和,如果mi不为负数,说明s[i]-mi会变小,即比s[i]本身小,倒不如直接用s[i],s[i]表示的就是前i项之和,也是区间和。

题目代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=1e5+10;
ll g[N],w[N],s[N],p[N];
ll sum,fix,mi;

int main(){
    int t;cin>>t;
    while(t--){
        int n;cin>>n;
        for(int i=1;i<=n;i++)cin>>g[i];
        for(int i=1;i<=n;i++)cin>>w[i];
        for(int i=1;i<=n;i++)
		{
			s[i]=s[i-1]+(g[i] == 1 ? -1 : 1)*w[i]; //计算出施加魔法后的偏移值(鸭子重量),如果原来是1(鸭子)就要减去,原来是0(鼠)就加上
		}
		sum=0;
        for(int i=1;i<=n;i++) {
            sum+=w[i]*g[i];
        }
        fix=0,mi=0;
        for(int i=1;i<=n;i++)
		{
			fix=max(fix,s[i]-mi);
			mi=min(mi,s[i]);
		}

        cout<<sum+fix<<'\n';
    }

    return 0;
}

代码有一个易错点,因为变量都是全局变量,所以sum,fix,e在每一组测试样例中都要重置为初始值。

import java.util.Scanner;
public class Main{


	static  int N=(int) (1e5+10);
	static long[] g=new long[N],w=new long[N],s=new long[N],p=new long[N];
	static long sum,fix,mi;
public static void main(String[] args) {
	 int t;
	 Scanner scanner = new Scanner(System.in);
	 t = scanner.nextInt();
	    while(t-- > 0){
	        int n;
	        n = scanner.nextInt();
	        for(int i=1;i<=n;i++)g[i]=scanner.nextLong();
	        for(int i=1;i<=n;i++)w[i]=scanner.nextLong();
	        for(int i=1;i<=n;i++)
			{
				s[i]=s[i-1]+(g[i] == 1 ? -1 : 1)*w[i]; //计算出施加魔法后的偏移值(鸭子重量),如果原来是1(鸭子)就要减去,原来是0(鼠)就加上
			}
			sum=0;
	        for(int i=1;i<=n;i++) {
	            sum+=w[i]*g[i];
	        }
	        fix=0;mi=0;
	        for(int i=1;i<=n;i++)  //从第一个遍历到最后,计算出最大最小值
			{
				fix=Math.max(fix,s[i]-mi);
				mi=Math.min(mi,s[i]);
			}
            System.out.println(sum+fix);
	    }

}
}

前缀和的拓展——压缩矩阵

最大子段和

题目描述

给出一个长度为 n n n 的序列 a a a,选出其中连续且非空的一段使得这段和最大。

输入格式

第一行是一个整数,表示序列的长度 n n n

第二行有 n n n 个整数,第 i i i 个整数表示序列的第 i i i 个数字 a i a_i ai

输出格式

输出一行一个整数表示答案。

样例输入

7
2 -4 3 -1 2 -4 3

样例输出

4

选取 [ 3 , 5 ] [3, 5] [3,5] 子段 { 3 , − 1 , 2 } \{3, -1, 2\} {3,1,2},其和为 4 4 4

数据规模

  • 对于 40 % 40\% 40% 的数据,保证 n ≤ 2 × 10 3 n \leq 2 \times 10^3 n2×103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 2 × 10 5 1 \leq n \leq 2 \times 10^5 1n2×105 − 10 4 ≤ a i ≤ 10 4 -10^4 \leq a_i \leq 10^4 104ai104
题目分析

提供一下类似动态规划的解法,而这个动态规划dp数组的定义类似最长上升子序列里面dp数组的定义。

第一阶段定义dp数组

直接定义dp数组。dp[i]表示以第i个字母结尾的最大字段和。

第二阶段推导状态转移方程

对于dp[i]我可以从两个地方转移过来,即以a[i-1]结尾的最大子段和加上a[i]和a[i]自己,那么从这两个转移状态里面选一个最大的就可以了,状态转移方程是下面的式子。

d p [ i ] = m a x ( d p [ i − 1 ] + a [ i ] , a [ i ] ) dp[i]=max(dp[i-1]+a[i],a[i]) dp[i]=max(dp[i1]+a[i],a[i])

第三阶段写代码

(1)dp数组初始化。这里求的是最大值,一般初始状态是为0,但是这里可能会出现负数的情况,要初始化为较小值吗?不需要,我们可以看一下 d p [ 1 ] = m a x ( d p [ 0 ] + a [ 1 ] , a [ 1 ] ) dp[1]=max(dp[0]+a[1],a[1]) dp[1]=max(dp[0]+a[1],a[1]),这里的dp[0]=0是对的,因为对于前0个数字,它的和就是0。每次dp[i]都会可能的值被更新,而dp[i+1]只用到了dp[i]。所以不用初始化。写的有点啰嗦了。

(2)递推dp数组。这里只需要遍历一次a数组就行了。

(3)答案。min(dp[i])。

题目代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
	Scanner scanner=new Scanner(System.in);
	int n = scanner.nextInt();
	int a[] = new int[n+1];
	int dp[] = new int[n+1];
	int ans = Integer.MIN_VALUE;
	for(int i = 1;i <= n;i++) {
		a[i] = scanner.nextInt();
		dp[i] = Math.max(dp[i-1]+a[i], a[i]);
		ans = Math.max(dp[i], ans);
	}
	System.out.println(ans);
}
}
最大加权矩形

题目描述

为了更好的备战 NOIP2013,电脑组的几个女孩子 LYQ,ZSC,ZHQ 认为,我们不光需要机房,我们还需要运动,于是就决定找校长申请一块电脑组的课余运动场地,听说她们都是电脑组的高手,校长没有马上答应他们,而是先给她们出了一道数学题,并且告诉她们:你们能获得的运动场地的面积就是你们能找到的这个最大的数字。

校长先给他们一个 n × n n\times n n×n 矩阵。要求矩阵中最大加权矩形,即矩阵的每一个元素都有一权值,权值定义在整数集上。从中找一矩形,矩形大小无限制,是其中包含的所有元素的和最大 。矩阵的每个元素属于 [ − 127 , 127 ] [-127,127] [127,127] ,例如

 0 –2 –7  0 
 9  2 –6  2
-4  1 –4  1 
-1  8  0 –2

在左下角:

9  2
-4  1
-1  8

和为 15 15 15

几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH,TZY 小朋友帮忙计算,但是遗憾的是他们的答案都不一样,涉及土地的事情我们可不能含糊,你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?

输入格式

第一行: n n n,接下来是 n n n n n n 列的矩阵。

输出格式

最大矩形(子矩阵)的和。

样例输入

4
0 -2 -7 0
 9 2 -6 2
-4 1 -4  1 
-1 8  0 -2

样例输出

15

数据规模

1 ≤ n ≤ 120 1 \leq n\le 120 1n120

题目分析

这道题是最大子段和的二维形式。可尝试把它压缩成一维。我们利用前缀和对矩阵进行行压缩。行压缩的结果是 a [ i ] [ j ] a[i][j] a[i][j]里面存的是前i行第j列的和,其实就是相对于第j列的前缀和。假设 a [ i ] [ j ] a[i][j] a[i][j]初始值是矩阵的值,那么类比于 s u m [ i ] = s u m [ i − 1 ] + a [ i ] sum[i]=sum[i-1]+a[i] sum[i]=sum[i1]+a[i]的前缀和求解方法,可以利用下式求前缀和,
a [ i ] [ j ] + = a [ i − 1 ] [ j ] a[i][j]+=a[i-1][j] a[i][j]+=a[i1][j]
求完前缀和后,如果我要求第j列,第l行到第r行的和,用 a [ r ] [ j ] − a [ l − 1 ] [ j ] a[r][j]-a[l-1][j] a[r][j]a[l1][j]表示。如果我要求以i行结尾的矩阵的最大值,那么应该怎么求?如下图所示,

原本我求第j列某几行的和,我需要依次相加,但是压缩后,我只需要用一个式子一下子就可以表示出来第j列某几行的和,现在相当于把矩阵压缩成了一维的形式,我只需要移动j就可以了。定义两个数组f[j]和dp[j],dp[j]的含义和最大子段和的含义一样,在行固定的情况下以第j列结尾的最大矩阵和,那么它的转移也和最大子段和一样,f[j]就表示在行固定的情况下第j列的矩阵和,它相当于最大子段和里面的a[i]。

对应的关键代码如下,

for(int i = 1;i <= n;i++) {
		for(int k = 1; k <= i;k++) {
			int f[] = new int[n+1];
			int dp[] = new int[n+1];
			for(int j = 1;j <= n;j++) {
				f[j] = a[i][j]-a[i-k][j];
				dp[j] = Math.max(dp[j-1]+f[j], f[j]);
				ans = Math.max(dp[j], ans);
			}
		}
	}

要想固定行,最低行和最高行都要固定,所以两层for循环是不可少的,固定了行之后再遍历列就可以了。

题目代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;

public class Main {
public static void main(String[] args) throws IOException {
	BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	Scanner scanner = new Scanner(System.in);
	int n = scanner.nextInt();
	int a[][] = new int[n+1][n+1];
	for(int i = 1;i <= n;i++){
		int id = 1;
		for(int j = 1;j <= n;j++) {
			a[i][j] = scanner.nextInt();
			a[i][j] += a[i-1][j];//利用前缀和进行矩阵的行压缩
		}
	}
	long ans = 0;
	for(int i = 1;i <= n;i++) {
		for(int k = 1; k <= i;k++) {
			int f[] = new int[n+1];
			int dp[] = new int[n+1];
			for(int j = 1;j <= n;j++) {
				f[j] = a[i][j]-a[i-k][j];
				dp[j] = Math.max(dp[j-1]+f[j], f[j]);
				ans = Math.max(dp[j], ans);
			}
		}
	}
	System.out.println(ans);
}
}

如果题目分析有错误或者没有表达清楚的地方,欢迎在评论区里指出!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值