for (int i = 0; i < n; i++) // 用两个for循环计算两两相乘,然后求和
for (int j = i + 1; j < n; j++)
s += a[i] * a[j];
cout << s << endl;
return 0;
}
复制代码
下面分析代码的时间和空间效率。 1)时间复杂度。代码实行了多少步骤?花了多少时间?
代码第8、9行有2层for循环,循环次数是: n − 1 + n − 2 + . . . + 1 ≈ n 2 / 2 n-1 + n-2 + ... +1 ≈ n^2/2 n−1+n−2+...+1≈n2/2,盘算复杂度为 O ( n 2 ) O(n^2) O(n2)。
对于30%的测试数据,n = 1000,循环次数$1000^2/2 = 50,000。盘算时间远远小于标题的时间限定1s,可以大概通过测试。
对于100%的测试数据,n = 200000,循环次数$200000 2 / 2 = 2 × 1 0 10 2/2 = 2×10^{10} 2/2=2×1010。盘算时间远大于标题的时间限定1s,超时不能通过测试。 2)空间复杂度,也就是步伐占用的内存空间。对于100%的数据,若用数组int a[200000]存储数据,int是32位整数,占用4个字节,所以int a[200000]共使用了800K空间,远小于标题的空间限定256MB。 (2)通过100%测试。本题使用前缀和,能得到100%的分数。
把盘算式子变更为:
S = ( a 1 + a 2 + . . . + a n − 1 ) × a n + ( a 1 + a 2 + . . . + a n − 2 ) × a n − 1 + ( a 1 + a 2 + . . . + a n − 3 ) × a n − 2 + . . . + ( a 1 + a 2 ) × a 3 + a 1 × a 2 S=(a_1+a_2 +...+a_{n-1})×a_n+(a_1+a_2 +...+a_{n-2})×a_{n-1}+(a_1+a_2+...+a_{n-3})×a_{n-2}+...+(a_1+a_2)×a_3+a_1×a_2 S=(a1+a2+...+an−1)×an+(a1+a2+...+an−2)×an−1+(a1+a2+...+an−3)×an−2+...+(a1+a2)×a3+a1×a2
此中括号内的部分是前缀和 s u m [ i ] = a 1 + a 2 + … + a i sum = a_1+a_2+…+ a_i sum=a1+a2+…+ai,把上式改为:
S = s u m [ n − 1 ] × a n + s u m [ n − 2 ] × a n − 1 + s u m [ n − 3 ] × a n − 2 + . . . + s u m [ 2 ] × a 3 + s u m [ 1 ] × a 2 S = sum[n-1] ×a_n + sum[n-2]×a_{n-1} + sum[n-3]×a_{n-2} + ... + sum[2]×a_3 + sum[1]×a_2 S=sum[n−1]×an+sum[n−2]×an−1+sum[n−3]×an−2+...+sum[2]×a3+sum[1]×a2
式子中用到的前缀和sum[1] ~ sum[n-1],用递推公式sum = sum[i-1] + a做一次for循环就能全部提前盘算出来。
下面的C++代码第8行先预盘算出前缀和sum[],然后使用sum[]求S。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n; //读n
vector<int> a(n+1,0); //定义数组a[],并初始化为0
for (int i=1; i<=n; i++) cin >> a[i]; // 读a[1]~a[n]
for (int i=1; i<=n; i++) a[i] = scanner.nextInt(); // 读取a[1]~a[n]
long[] sum = new long[n+1]; // 定义前缀和数组 sum[],并初始化为0
for (int i=1; i<n; i++) sum[i]=a[i]+sum[i-1]; // 预计算前缀和sum[1]~sum[n-1]
long s = 0;
for (int i=1; i<n; i++) s+=sum[i]*a[i+1]; // 计算和s
System.out.println(s);
}
}
复制代码
python代码
n = int(input())
a = [0]+[int(i) for i in input().split()] #读入a[1]~a[n]。a[0]不用
sum = [0] * (n+1) #定义前缀和
sum[1] = 0
for i in range(1,n): sum[i] = a[i]+sum[i-1] #预计算前缀和sum[1]~sum[n-1]
s = 0
for i in range(1,n): s += sum[i]*a[i+1] #计算和s
print(s)
复制代码
例2 基本应用
可得到的最小取值
第一步肯定是排序,例如从小到大排序,然后再进行两种操作。操作(1)在a[]的尾部选一个数,操作(2)在a[]的头部选2个数。
轻易想到一种简单方法:每次在操作(1)和操作(2)中选较小的值。这是贪心法的思路。但是贪心法对吗?分析之后发现贪心法是错误的,例如{3, 1, 1, 1, 1, 1, 1},做k=3次操作,每次都按贪心法,做3次操作(2),结果是6。但是准确答案是做3次操作(1),结果是5。
转头重新考虑所有可能的情况。设操作(2)做p次,操作(1)做k-p次,求和:
∑ i = 1 2 p a i + ∑ i = n + p − k + 1 n a i \sum_{i=1}^{2p}a_i+\sum_{i={n+p-k+1}}^{n}a_i ∑i=12pai+∑i=n+p−k+1nai
为了找最小的和,需要把所有的p都试一遍。假如直接按上面的公式盘算,那么验证一个p的盘算量是O(n)的,验证所有的p,1≤p≤k,总盘算量O(kn),超时。
轻易发现公式的两个部分就是前缀和,分别即是sum[2p]、sum[n]-sum[n+p-k]。假如提前算出前缀和sum[],那么验证一个p的时间是O(1)的,验证所有p的总盘算量是O(n)的。
下面是C++代码。注意sum[]需要用long long范例。
代码的盘算复杂度,第10行sort()是O(nlogn),第13行是O(n),总复杂度为O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 200010;
long long a[N],s[N]; //s[]是a[]的前缀和
int main(){
int n, k; cin >> n >> k;
for (int i = 1; i <= n; i++) scanf("%lld",&a[i]);
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) s[i] = s[i-1]+ a[i];
ll ans = 1e18;
for (int p = 1; p <= k; p++)
ans = min(s[n] - s[n+p-k] + s[2*p], ans);
printf("%lld",ans);
return 0;
}
复制代码
java代码
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
long[] a = new long[n+1];
for (int i = 1; i <= n; i++) a[i] = scanner.nextLong();
Arrays.sort(a, 1, n+1);
long[] s = new long[n+1];
for (int i = 1; i <= n; i++) s[i] = s[i-1] + a[i];
long ans = (long)1e18;
for (int p = 1; p <= k; p++)
ans = Math.min(s[n] - s[n+p-k] + s[2*p], ans);
System.out.println(ans);
}
}
复制代码
Python代码
n, k = map(int, input().split())
b = list(map(int, input().split()))
a=[0] + sorted(b) # a[0]不用,从a[1]开始
s = [0] * (n+1)
for i in range(1, n+1): s[i] = s[i-1] + a[i]
ans = 10**18
for p in range(1, k+1):
ans = min(s[n] - s[n+p-k] + s[2*p], ans)
print(ans)
复制代码
例3 异或的前缀和
下面例题是前缀和在异或盘算中的应用,也是常见的应用场景。
异或和之和
n个数 a 1 − a n a_1-a_n a1−an的异或和即是: a 1 ⊕ a 2 ⊕ . . . ⊕ a n a_1⊕a_2⊕...⊕a_n a1⊕a2⊕...⊕an。
(1)通过30%的测试。
本题的简单做法是直接按题意盘算所有子段的异或和,然后加起来。
有多少个子段?
长度为1的子段异或和有n个: a 1 、 a 2 、 . . . 、 a n a_1、a_2、...、a_n a1、a2、...、an
长度为2的子段异或和有n-1个: a 1 ⊕ a 2 、 a 2 ⊕ a 3 、 . . . a n − 1 ⊕ a n a_1⊕a_2、a_2⊕a_3、...a_{n-1}⊕a_n a1⊕a2、a2⊕a3、...an−1⊕an
…
长度为n的子段异或和有1个: a 1 ⊕ a 2 ⊕ a 3 ⊕ . . . ⊕ a n − 1 ⊕ a n a_1⊕a_2⊕a_3⊕...⊕a_{n-1}⊕a_n a1⊕a2⊕a3⊕...⊕an−1⊕an
共 n 2 / 2 n^2/2 n2/2个子段。
下面代码第8、9行遍历所有的子段[L, R],第11行求[L,R]的字段和。共3重for循环,盘算复杂度 O ( n 3 ) O(n^3) O(n3),只能通过30%的测试。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n; cin >> n;
vector<int> a(n); //用vector定义数组a[]
for (int i = 0; i < n; i++) cin >> a[i];
long long ans=0; //注意这里用long long
for(int L=0;L<n;L++) //遍历所有区间[L,R]
for(int R=L;R<n;R++){
int sum=0;
for(int i=L;i<=R;i++) sum^=a[i]; //子段和
ans += sum; //累加所有子段和
}
cout<<ans;
return 0;
}
复制代码
(2)通过60%的测试。本题可以用前缀和优化。
记异或和 a 1 ⊕ a 2 ⊕ . . . ⊕ a i a_1⊕a_2⊕...⊕a_i a1⊕a2⊕...⊕ai的前缀和为:
s i = a 1 ⊕ a 2 ⊕ . . . ⊕ a i si=a_1⊕a_2⊕...⊕a_i si=a1⊕a2⊕...⊕ai
这里 s i s_i si是异或形式的前缀和。这样就把复杂度为O(n)的子段异或和盘算 a 1 ⊕ a 2 ⊕ . . . ⊕ a i a_1⊕a_2⊕...⊕a_i a1⊕a2⊕...⊕ai,优化到了O(1)的求 s i s_i si的盘算。
以包罗 a 1 a1 a1的子段为例,这些子段的异或和相加,即是:
a 1 + a 1 ⊕ a 2 + . . . + a 1 ⊕ . . . ⊕ a i + . . . + a 1 ⊕ . . . ⊕ a n = s 1 + s 2 + . . . + s i + . . . + s n a_1+a_1⊕a_2+...+a_1⊕...⊕a_i+...+a_1⊕...⊕a_n = s_1+s_2+...+s_i+...+s_n a1+a1⊕a2+...+a1⊕...⊕ai+...+a1⊕...⊕an=s1+s2+...+si+...+sn
前缀和的盘算用递推得到。普通前缀和的递推公式为 s [ i ] = s [ i − 1 ] + a [ i ] s = s[i-1] + a s=s[i−1]+a,异或形式的前缀和递推公式为s = s[i-1] ^ a,下面代码第11行用这个公式的简化形式求解了前缀和。
代码的盘算复杂度是多少?第8行和第10行用两重循环遍历所有的子段,同时盘算前缀和,盘算复杂度是 O ( n 2 ) O(n^2) O(n2)的,可以通过60%的测试。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
long long ans = 0;
for (int L = 0; L < n; L++) {
long sum = 0; //sum是包含a[L]的子段的前缀和
for (int R = L ; R < n; R++) {
sum ^= a[R]; //用递推求前缀和sum
ans += sum; //累加所有子段和
}
}
cout << ans << endl;
return 0;
}
复制代码
(3)通过100%的测试。
本题有没有进一步的优化方法?这就需要仔细分析异或的性子了。根据异或的界说,有a⊕a = 0、0⊕a = a、0⊕0 = 0。推导子段 a i a j a_i ~ a_j ai aj的异或和:
a i ⊕ a i + 1 ⊕ . . . ⊕ a j − 1 ⊕ a j = ( a 1 ⊕ a 2 ⊕ . . . ⊕ a i − 1 ) ⊕ ( a 1 ⊕ a 2 ⊕ . . . ⊕ a j ) a_i⊕a_{i+1}⊕...⊕a_{j-1}⊕a_j = (a_1⊕a_2⊕...⊕a_{i-1}) ⊕ (a_1⊕a_2⊕...⊕a_j) ai⊕ai+1⊕...⊕aj−1⊕aj=(a1⊕a2⊕...⊕ai−1)⊕(a1⊕a2⊕...⊕aj)
记 s i = a 1 ⊕ a 2 ⊕ . . . ⊕ a i s_i = a_1⊕a_2⊕...⊕a_i si=a1⊕a2⊕...⊕ai,这是异或形式的前缀和。上式转化为:
a i ⊕ a i + 1 ⊕ . . . ⊕ a j − 1 ⊕ a j = s i − 1 ⊕ s j a_i⊕a_{i+1}⊕...⊕a_{j-1}⊕a_j = s_{i-1}⊕s_j ai⊕ai+1⊕...⊕aj−1⊕aj=si−1⊕sj
若 s i − 1 = s j s_{i-1} = s_j si−1=sj,则 s i − 1 ⊕ s j = 0 s_{i-1}⊕s_j = 0 si−1⊕sj=0;若 s i − 1 ≠ s j s_{i-1} ≠ s_j si−1=sj,则 s i − 1 ⊕ s j = 1 s_{i-1}⊕s_j =1 si−1⊕sj=1。标题要求所有子段异或和相加的结果,这即是判定所有的 s i , s j {s_i, s_j} si,sj组合,若 s i ≠ s j s_i ≠ s_j si=sj,则结果加1。
怎样判定两个s是否相当?可以用位操作的技巧,假如它们的第k位不同,则两个s肯定不等。下面以 a 1 = 011 , a 2 = 010 a_1 = 011,a_2 = 010 a1=011,a2=010为例,分别盘算第k位的异或和,而且相加:
k=0,第0位异或和, s 1 = 1 , s 2 = 1 ⊕ 0 = 1 , a n s 0 = a 1 + a 2 + a 1 ⊕ a 2 = s 1 + s 1 ⊕ s 2 + s 2 = 1 + 0 + 1 = 2 s_1=1,s_2=1⊕0=1,ans_0 = a_1+a_2+a_1⊕a_2 = s_1+s_1⊕s_2+s_2 = 1+0+1=2 s1=1,s2=1⊕0=1,ans0=a1+a2+a1⊕a2=s1+s1⊕s2+s2=1+0+1=2
k=1,第1位异或和, s 1 = 1 , s 2 = 1 ⊕ 1 = 0 , a n s 1 = a 1 + a 2 + a 1 ⊕ a 2 = s 1 + s 1 ⊕ s 2 + s 2 = 1 + 1 + 0 = 2 s_1=1,s_2=1⊕1=0,ans_1 = a_1+a_2+a_1⊕a_2 = s_1+s_1⊕s_2+s_2 = 1+1+0=2 s1=1,s2=1⊕1=0,ans1=a1+a2+a1⊕a2=s1+s1⊕s2+s2=1+1+0=2
k=2,第2位异或和, s 1 = 0 , s 2 = 0 ⊕ 0 = 0 , a n s 2 = a 1 + a 2 + a 1 ⊕ a 2 = s 1 + s 1 ⊕ s 2 + s 2 = 0 + 0 + 0 = 0 s_1=0,s_2=0⊕0=0,ans_2 = a_1+a_2+a_1⊕a_2 = s_1+s_1⊕s_2+s_2 = 0+0+0=0 s1=0,s2=0⊕0=0,ans2=a1+a2+a1⊕a2=s1+s1⊕s2+s2=0+0+0=0
最后盘算答案: a n s = a n s 0 × 2 0 + a n s 1 × 2 1 + a n s 2 × 2 2 = 6 ans = ans_0 ×2^0+ ans_1×2^1 + ans_2×2^2 = 6 ans=ans0×20+ans1×21+ans2×22=6。
本题 0 ≤ A i ≤ 2 20 0≤A_i≤2^{20} 0≤Ai≤220,所有的前缀和s都不高出20位。代码第8行逐个盘算20位的每一位,第11行for循环盘算n个前缀和,总盘算量约为20×n。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
long long ans = 0;
for(int k=0;k<=20;k++){ //所有a不超过20位
int zero=1,one=0; //统计第k位的0和1的数量
long long cnt=0,sum=0; //cnt用于统计第k位有多少对si⊕sj =1
for(int i=0;i<n;i++){
int v=(a[i]>>k)&1; //取a[i]的第k位
sum ^= v; //对所有a[i]的第k位做异或得到sum,sum等于0或者1
if(sum==0){ //前缀和为0
zero++; //0的数量加1
cnt += one; //这次sum=0,这个sum跟前面等于1的sum异或得1
}else{ //前缀异或为1
one++; //1的数量加1
cnt += zero; //这次sum=1,这个sum跟前面等于0的sum异或得1
}
}
ans += cnt*(1ll<<k); //第k位的异或和相加
}
cout<<ans;
return 0;
}
复制代码
java代码
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];
for (int i = 0; i < n; i++) a[i] = scanner.nextInt();
long ans = 0;
for (int k = 0; k <= 20; k++) { // 所有a不超过20位
int zero = 1, one = 0; // 统计第k位的0和1的数量
long cnt = 0, sum = 0; //cnt用于统计第k位有多少对si⊕sj =1
for (int i = 0; i < n; i++) {
int v = (a[i] >> k) & 1; // 取a[i]的第k位
sum ^= v; // 对所有a[i]的第k位做异或得到sum,sum等于0或者1
if (sum == 0) { // 前缀和为0
zero++; // 0的数量加1
cnt += one; // 这次sum=0,这个sum跟前面等于1的sum异或得1
} else { // 前缀异或为1
one++; // 1的数量加1
cnt += zero; // 这次sum=1,这个sum跟前面等于0的sum异或得1
}
}
ans += cnt * (1L << k); // 第k位的异或和相加
}
System.out.println(ans);
}
}
复制代码
Python代码
n = int(input())
a = [int(x) for x in input().split()]
ans = 0
for k in range(21): # 所有a不超过20位
zero, one = 1, 0 # 统计第k位的0和1的数量
cnt, sum = 0, 0 #cnt用于统计第k位有多少对si⊕sj =1
for i in range(n):
v = (a[i] >> k) & 1 # 取a[i]的第k位
sum ^= v # 对所有a[i]的第k位做异或得到sum,sum等于0或者1
if sum == 0: # 前缀和为0
zero += 1 # 0的数量加1
cnt += one # 这次sum=0,这个sum跟前面等于1的sum异或得1
else: # 前缀异或为1
one += 1 # 1的数量加1
cnt += zero # 这次sum=1,这个sum跟前面等于0的sum异或得1
ans += cnt * (1 << k) # 第k位的异或和相加
print(ans)
复制代码
例4 二维前缀和
前面的例子都是一位数组上的前缀和,下面介绍二维数组上的前缀和。
例题:领地选择
概况题意:在n×m 的矩形中找一个边长为c的正方形,把正方形内所有坐标点的值相加,使价值最大。
简单的做法是罗列每个坐标,作为正方形左上角,然后算出边长c内所有地块的价值和,找到价值和最高的坐标。时间复杂度 O ( n × m × c 2 ) O(n×m×c^2) O(n×m×c2),能通过60%的测试。请读者练习。
本题是二维前缀和的直接应用。
一维前缀和界说在一维数组a[]上:sum = a[1] + a[2] + … + a
把一维数组a[]当作一条直线上的坐标,前缀和就是所有坐标值的和。