【算法专题二十七】前缀和&差分思想

发布于:2025-03-04 ⋅ 阅读:(10) ⋅ 点赞:(0)

目录

一维前缀和

那么前缀和有什么作用呢?!!!

学会了一维前缀和???那么直接上强度!!!

一维差分

差分是什么?

先说结论:

二维前缀和

二维差分

先说结论:

步骤:

本章节对我的收获很大,希望对你也是~~~~


一维前缀和

如果直到原数组arr的所有元素,那么我们只需要重新构造一个前缀和数组nums即可,从下标1可以开始进行赋值,每次nums[i] 得到的结果就是【0 ~ i】 下标的所有arr数组的和,即arr数组的前缀和!

那么前缀和有什么作用呢?!!!

当我们想要知道一维数组的某个连续的区间的数组和,就可以直接在O(1) 的时间复杂的来直接进行获取该值,eg:

int sum = nums[2] - nums[0]; // 等价于 arr[1] + arr[2]

就表示的是下标为2的位置 减去 下标为0 位置的前缀和数组,得到的结果就是原数组arr[1] + arr[2]的结果。

由此就可以看出,前缀和数组,是在以空间换取时间的前提下,O(n)下遍历数组,得到一个空间复杂度也为O(n) 的nums数组,在O(1) 的条件下,直接得到关于[l,r]区间内的arr数组的和。

void fn() {
	vector<int> arr = {0,1,2,3,4,5};
	vector<int> nums(arr.size(),0);
	for(int i = 1; i < arr.size(); i++) {
		nums[i] = nums[i - 1] + arr[i];
	}
	for(auto e : nums) cout<<e<<" ";cout<<endl; // 0 1 3 6 10 15
}

由于nums会存在[i - 1] 区间,那么一般下标都是从1开始,防止越界,所以只要将arr数组从所有元素往后移动一位,也从1开始,就正好实现了nums 与 arr数组的下标对应;

学会了一维前缀和???那么直接上强度!!!

重新排序:蓝桥杯一道经典的前缀和算法题,直接暴力90%的测试用例!重新排序

题目意思也很好理解,就是用最优的排序,来比较排序前后所选择的所有区间内,数字总和的最大差值!

细心的小伙伴这个时候就能想到啦,只要每次排序都记录重复次数最多的区间,然后把最大值放到这个重复次数最多的区间就可以啦! 没错这就是暴力前缀和的思想,只需要求出一个前缀和数组nums,然后重复m次,选择区间[l,r]内的数字总和时,在O(1)下就能得到结果,sum = nums[r] - nums[l - 1];

最关键的就是,要记录每个区间重复的次数,这里就要开辟新的数组来记录重复出现的次数,在重复得到m次区间sum的条件下,又要在 r - l 的次数内记录当前下标出现的次数在index数组内,也就是O(m * n) 的时间复杂度,这里就会发现,过不了最后一个测试用例!!!(超时)

那么我们上面本身就是要遍历数组是O(n),如果在m次操作下,任然要再次遍历数组来记录重复出现的下标次数,未免时间复杂度太高,也就是在这里进行优化,也就是后面要进行学习的差分思想~~~~ 现在可以尝试着手撕暴力代码,然后继续学习后面的差分思想

#include <bits/stdc++.h>

using namespace std;

#define int long long

void fn() {
    int n;cin>>n;
    vector<int> nums(n + 1);
    auto a = nums;
    auto b = nums;
    auto nums_begin = nums;
    
    for(int i = 1; i <= n; i++) {
        cin>>b[i];
        nums[i] = nums[i - 1] + b[i];
    }
    vector<int>  index(n + 1);  //记录重复最多的位置 
    int ret_min = 0;
    int m;cin>>m;
    int p = m;
    vector<int> ll;
    auto rr = ll;
    while(m--) {
        int l,r;cin>>l>>r;
        ll.push_back(l);
        rr.push_back(r);
        int sum = nums[r] - nums[l - 1];
        ret_min += sum;
        for(int i = l; i <= r; i++) index[i]++;
    }
    map<int,vector<int>> hash;
    for(int i = 0; i < index.size(); i++) {
        hash[index[i]].push_back(i);
    }
    
    
//    for(auto& e : hash) {
//        cout<<e.first<<":";
//        for(auto& k : e.second) {
//            cout<<k<<" "; 
//        }
//        cout<<endl;
//    }
    
    sort(b.begin(),b.end(),greater<int>());
//    for(auto e : nums) cout<<e<<" ";cout<<endl<<endl;
    
    for(auto& e : hash) {
        for(auto& k : e.second) {
            a[k] = b.back();
            b.pop_back();
        }
    }
    
//    for(auto e : a) cout<<e<<" ";cout<<endl;
//    cout<<endl;

    int rep = 0;
    for(int i = 1; i <= n; i++) {
        nums_begin[i] = nums_begin[i-1] + a[i];
    }
    
    int ret_max = 0;
    for(int i = 0; i < ll.size(); i++) {
        int sum = nums_begin[rr[i]] - nums_begin[ll[i] - 1];
        ret_max += sum;
    }
    
    cout<<ret_max - ret_min<<endl;
        
} 

signed main() {
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int t = 1;while(t--) fn();
    
    return 0;
}

一维差分

差分是什么?

以O(n)的时间复杂度 ,在数组arr内进行m次修改 不同 [l, r]区间内的值;

一般进行这样的操作,正常来说就是遍历m次,然后再遍历数组从i = l 开始到 i = r结束来进行修改数组,也就是说O(m * n)的条件下才能完成这个操作

先说结论:

[L, R] + V    <=>   d[L] + V ,d[R] - V

可能你现在看不懂,没关系,看到下面的讲解,你会豁然开朗,也许你会跟我一样,认为这是哪个天才想到的这么完美的办法~~~~

这里只是操作了一次,那么我们将他推广到3次或者更多次不同区间的操作:

那么这里就能看出,上面给出的结论是正确的,不需要再次遍历数组,而只是需要求出每次的差值进行计算d[l] + v,d[r + 1] - v即可!!!那么他的原理是什么呢???

从图中我们可以看出差分数组d从下标L开始进行改变,那么前缀和数组sumd 后续的值都会跟着进行改变,知道我们想要停止的那个区间R后面的一个值进行减掉相应的值既可停止后续前缀和的改变!!!也就是说现在我们就能理解为什么会有当前公式的存在~~~~直接改变差分数组的变量值,操作m次后,最后再求差分数组的前缀和sumd数组也就是修改不同区间的值后最后能够得到的修改结果,由于最后要遍历差分数组d,最后的时间复杂度就是O(n)

那么通过上面的学习也不知道你是否有跟我一样的感受,被当前的操作给惊艳到!下面就是上面题目的优化代码:

核心:我们的目的还是为了求出index数组,也就是求出最后操作m次不同区间数组的最终修改值,那么我们就用当前差分思想,能够将原来的时间复杂度O(m * n) 优化到O(n) 当m足够大的时候这总优化也是一种质的飞跃~~~~

	while(m--) {
		int l,r;cin>>l>>r;
		ll.push_back(l);
		rr.push_back(r);
		int sum = nums[r] - nums[l - 1];  // 前缀和相减 
		ret_min += sum;
//		for(int i = l; i <= r; i++) index[i]++;  // 暴力 
		
		d[l]++;
    if(r + 1 <=  n) d[r+1]--;
	}
	
	
	for(int i = 1; i <= n; i++) {
		index[i] = index[i - 1] + d[i]; // 修改后的前缀和 
	}	

最终:ac代码的结果,整体来说大差不差,只是多了一个差分的优化思想!

#include <bits/stdc++.h>

using namespace std;

#define int long long

void fn() {
	int n;cin>>n;
	vector<int> nums(n + 1);  //前缀和数组 
	auto a = nums; //排序后数组 
	auto b = nums; //原始数组 
	auto nums_begin = nums;
	auto d = nums;
	auto sumd = nums;
  auto index = nums;
	
	for(int i = 1; i <= n; i++) {
		cin>>b[i];
		nums[i] = nums[i - 1] + b[i];
	}
	
	
	// vector<int>  index(n + 1);  //记录重复最多的位置 
	int ret_min = 0;
	int m;cin>>m;
	int p = m;
	vector<int> ll;
	auto rr = ll;
	
	
	
	
	while(m--) {
		int l,r;cin>>l>>r;
		ll.push_back(l);
		rr.push_back(r);
		int sum = nums[r] - nums[l - 1];  // 前缀和相减 
		ret_min += sum;
//		for(int i = l; i <= r; i++) index[i]++;  // 暴力 
		
		d[l]++;
    if(r + 1 <=  n) d[r+1]--;
	}
	

	
	for(int i = 1; i <= n; i++) {
		index[i] = index[i - 1] + d[i]; // 修改后的前缀和 
	}	
	
//		for(auto e : index) cout<<e<<" ";cout<<endl;
		
	map<int,vector<int>> hash;
	// 重新排序 记录出现次数最多的位置 
	for(int i = 0; i < index.size(); i++) {
		hash[index[i]].push_back(i);
	}

//	for(auto e : sumd) cout<<e<<" ";cout<<endl; 

	sort(b.begin(),b.end(),greater<int>());
//	for(auto e : nums) cout<<e<<" ";cout<<endl<<endl;
	
	for(auto& e : hash) {
		for(auto& k : e.second) {
			a[k] = b.back();
			b.pop_back();
		}
	}
	
//	for(auto e : a) cout<<e<<" ";cout<<endl;
//	cout<<endl;

	int rep = 0;
	for(int i = 1; i <= n; i++) {
		nums_begin[i] = nums_begin[i-1] + a[i];
	}
	
	int ret_max = 0;
	for(int i = 0; i < ll.size(); i++) {
		int sum = nums_begin[rr[i]] - nums_begin[ll[i] - 1];
		ret_max += sum;
	}
	
	cout<<ret_max - ret_min<<endl;
		
} 

signed main() {
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	int t = 1;while(t--) fn();
	
	return 0;
} 

二维前缀和

从图中可以看出,要求出二维数组前缀和就是当前位置[i , j] 的左侧方块nums[i][j - 1] + 上侧方块nums[i - 1][j]然后再减去二者方块重复的面积(粉色面积) 加上当前放各的大小即可。

一般,我们都会为原数组都开一行 和 一列,这样求出前缀和数组nums的时候就会防止越界,arr[i][j]的下标就会一一对应,如果没有多开一行 和 一列,那么计算前缀和数组的时候就只需要单独考虑数组越界情况arr[i - 1][j - 1];

	vector<vector<int>> arr(5,vector<int>(5));
	auto nums = arr;
	
	for(int i = 1; i <= 4; i++) {
		for(int j = 1; j<= 4; j++) {
			arr[i][j] = i + j - 1 - 1;
		}
	}
	
	for(int i = 1; i <= 4; i++) {
		for(int j = 1; j <= 4; j++) {
			nums[i][j] = nums[i - 1][j] + nums[i][j - 1] - nums[i - 1][j - 1] + arr[i][j];
		}
	}
	
	for(int i = 0; i <= 4; i++) {
		for(int j = 0; j <= 4; j++) {
			cout<<nums[i][j]<<" ";
		}
		cout<<endl;
	}
	
	
//	for(int i = 0; i <= 4; i++) {
//		for(int j = 0; j <= 4; j++) {
//			cout<<arr[i][j]<<" ";
//		}
//		cout<<endl;
//	}

可以看出二维前缀和数组跟一维前缀和数组没有什么区别,都是对当前位置的值 再加上前面一层和一列所包含的数组大小

那么就拥有跟一维前缀和数组一样的性质,就是再O(n * n) 的时间复杂度的条件下,来构建这个二维前缀和数组,可以再O(1) 的条件下来得到关于任意两点之间所围成的面积大小:

当我想要得到红色框框所框选的数字和,那么就可以再O(1)下通过加减法直接得到:

	int x1,x2,y1,y2;cin>>x1>>y1>>x2>>y2;
	int sum = nums[x2][y2] - nums[x2][y1-1] - nums[x1-1][y2] + nums[x1-1][y1-1];

二维差分

学会了一维差分,二维差分就非常简单了~~~~

先说结论:

d[x1][y1] += v
d[x2+1][y1] -= v
d[x1,y2+1] -= v
d[x2+1][y2+1] += v

如图:

当我按照差分思想构造差分数组的时候,对某一个范围[x1][y1]~[x2][y2]这个矩形范围进行修改不同的值的时候,我就要考虑后续的其他地方不会被我修改到,也就是要将这个矩形的三个角落进行还原,看图就能非常的清晰观察到,最后有一块矩形的地方被重复-1,要将他还原只能再次+1

步骤:

1.首先就是创建新的差分数组d,然后进行填表,按照[L,R]m次的不同区间进行差分;

2.得到d后创建index前缀和差分数组;

3.最后一步,与原数组结合,得到最终的修改数组

 

	vector<vector<int>> arr(5,vector<int>(5));
	auto nums = arr;
	auto d = arr;
	auto index = arr;
	
	arr = {
	{0,0,0,0,0},
	{0,1,5,6,8},
	{0,9,6,7,3},
	{0,5,3,2,4}	
	};
	
	for(int i = 0; i < 4; i++) {
		for(int j = 0; j < 5; j++) {
			cout<<arr[i][j]<<" ";
		}
		cout<<endl;
	}
	
	int m;cin>>m;
	while(m--) {
		int x1,y1,x2,y2,v;cin>>x1>>y1>>x2>>y2>>v;
		d[x1][y1] += v;
		if(x2 + 1 < 4) d[x2+1][y1] -= v;
		if(y2 + 1 < 5) d[x1][y2+1] -= v;
		if(x2 + 1 < 4 && y2 + 1 < 5) d[x2 + 1][y2 + 1] += v;
	}
	cout<<endl;
	
	for(int i = 0; i < 4; i++) {
		for(int j = 0; j < 5; j++) {
			cout<<d[i][j]<<" ";
		}
		cout<<endl;
	}
	
	for(int i = 1; i < 4; i++) {
		for(int j = 1; j < 5; j++) {
			index[i][j] = index[i-1][j] + index[i][j-1] - index[i-1][j-1] + d[i][j];
		}
	}
	cout<<endl;
	
	for(int i = 0; i < 4; i++) {
		for(int j = 0; j < 5; j++) {
			cout<<index[i][j]<<" ";
		}
		cout<<endl;
	}
	
	for(int i = 0; i < 4; i++) {
		for(int j = 0; j < 5; j++) {
			arr[i][j] += index[i][j];
		}
	}
	
	cout<<endl;

	for(int i = 0; i < 4; i++) {
		for(int j = 0; j < 5; j++) {
			cout<<arr[i][j]<<" ";
		}
		cout<<endl;
	}	

本章节对我的收获很大,希望对你也是~~~~