区间操作の神の双手:从入门到入坟的前缀和差分指南

        今天咱们要聊的是算法界的"孪生兄弟"——前缀和与差分。这俩货简直就是处理数组问题的"瑞士军刀",一个负责扮猪吃老虎,一个擅长暗度陈仓。准备好瓜子饮料,咱们这就开车!

一、前缀和:你的私人记账小秘书

1.1 前缀和思想

想象你是个每天剁手的败家子,现在老妈要查你过去30天的支付宝账单:"从双十一到黑五总共花了多少?"

这时候你有两种选择:

  • 暴力解法:把11.11到11.25的账单一条条加起来(手抽筋警告⚠️)

  • 聪明做法:提前做好"消费累计表",查账时直接大数减小数(深藏功与名✨)

这就是前缀和的核心思想用空间换时间把O(n)的查询变成O(1)可以在暴力枚举的过程中 快速查询出一段区间的结果。

1.2 一维前缀和模板

【模板】前缀和

【解题】:

1.看到本题的第一反应就是暴力解法:每次区间询问的时候,从左端点到右端点依次累加,然后的出结果。很显然这种思想的时间复杂度时O(n^2)不能通过此题。

2.前缀和解法:多开一个数组f[i],表示从 1-i 位置的区间和,f[5] 表示从a[1] 加到a[5],所以它的递推公式很简单f[i] = f[i - 1] + a[i]; 然后查询的时候直接f[r] - f[l - 1];

🖥️code:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

typedef long long LL;
LL s[N];
int a[N];

int main()
{
	int n, q; cin >> n >> q;
	for(int i = 1; i <= n; i++) cin >> a[i];
	
	// 预处理
	for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
	
	while(q--)
	{
		int l, r; cin >> l >> r;
		cout << s[r] - s[l - 1] << endl;	
	} 
	
	
	return 0;
}

1.3 一维前缀和例题

P1115 最大子段和 - 洛谷

【解题】:这一题是非常经典的一道题,前缀和,贪心,分治,DP都可以解这道题。

预处理前缀和数组,但是枚举所有区间会超出内存限制,子段和最大左边界的前缀和一定最小,所以我们多维护一个prevmin表示 1-i 位置的最小的前缀和。

🖥️code:

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 2e5 + 10;
LL n, a[N];

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> a[i];
		a[i] = a[i - 1] + a[i];
	}
	LL ret = -1e17;
	LL prevmin = 0;
	for(int i = 1; i <= n; i++)
	{
		ret = max(ret, a[i] - prevmin);
		prevmin = min(prevmin, a[i]);
	}
	cout << ret << endl;
	return 0;
} 

1.4 二维前缀和模板

【模板】二维前缀和

【解题】:同上述一维前缀和,暴力解法过不了, 我们还是利用前缀和的思想预处理数组。

此时前缀和数组 f[i][j] 表示从1,1 到 i,j位置矩形内的数组和。

下面是推导转移公式 f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];

下面是推导区间公式x1,y1 到 x2,y2 的区域数组和,f[x2][y2] - f[x2][y1 - 1] - f[x1 - 1][y2] + f[x1 - 1][y1 - 1];

 

🖥️code:

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1000 + 10;
int a[N][N];
// 二位前缀和 
LL s[N][N];
int q, n, m;

int main()
{
	cin >> n >> m >> q;
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= m; j++)	
		{
			cin >> a[i][j];
		}
	}
		
	// 预处理二位前缀和
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= m; j++)
		{
			s[i][j]	= a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
		}	 
	} 
	while(q--)
	{
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;
		cout << s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] << endl;
	}
	
	return 0;
 } 

1.5 二维前缀和例题

P2280 [HNOI2003] 激光炸弹 - 洛谷

【解题】:其实就是求m* m最大的区间和啦。

🖥️code:

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 5e3 + 10;
LL f[N + 1][N + 1], a[N + 1][N + 1];
int n, m;

int main()
{
	cin >> n >> m;
	while (n--)
	{
		int x, y, v; cin >> x >> y >> v;
		x++; y++; // 从1开始计数
		a[x][y] += v; // 可能不止一个物品 
	}
	n = 5010;
	// 前缀和 
	for (int i = 1; i <= N; i++)
	{
		for (int j = 1; j <= N; j++)
		{
			f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];
		}
	}
	LL ret = 0;
	for (int x1 = 1; x1 + m - 1 <= N; x1++)
	{
		for (int y1 = 1; y1 + m - 1 <= N; y1++)
		{
			int x2 = x1 + m - 1, y2 = y1 + m - 1;
			LL t = f[x2][y2] - f[x2][y1 - 1] - f[x1 - 1][y2] + f[x1 - 1][y1 - 1];
			ret = max(ret, t);
		}
	}
	cout << ret << endl;
	return 0;
}

二、差分:时空穿越的魔法师

2.1 差分思想

假设你是植物大战僵尸里的园丁,要给第3到第5行的向日葵各浇10次水。这时候僵尸已经走到家门口了,你怎么办?

  • 老实人做法:挨个给3、4、5行浇水(僵尸:真慢,开饭啦!)

  • 聪明做法:在3行标记"+10",6行标记"-10",最后统一施法(僵尸:我眼花了?)

 前缀和和查分运算是一对互逆运算。

2.2 一维差分模板

【模板】差分

【解题】:每次操作都要在一段区间加上一个数。我们发现在这个区间加上数的时候,前后的差值不变,换句话说,差分数组就是差值的数组,l,r区间加b,就在差分数组 l位置+b,r+1位置-b。

1. 创建差分数组:

根据定义: f[i] = a[i] − a[i − 1]
也可以根据差分数组的性质: f[i] + =  a[i],  f[i + 1] − =  a[i](这个可以理解成自己对自己差分,来了一个a[i]后,他与前面的差值f[i] + a[i],与后面的差值f[i + 1] - a[i])。

2. 对区间端点修改

3. 对差分数组求前缀和,还原数组。

🖥️code:

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;
int n, m;
LL a[N], f[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) 
    {
        cin >> a[i];
        f[i] += a[i];
        f[i + 1] -= a[i];
    }
    while(m--)
    {
        int l, r, x; cin >>  l >> r >> x;
        f[l] += x;
        f[r + 1] -= x;
    }
    for(int i = 1; i <= n; i++) 
    {
        f[i] += f[i - 1];
        cout << f[i] << " ";
    }
    return 0;
}

2.3 一维差分例题

P3406 海底高铁 - 洛谷

【解题】:问题的关键在于,对于每个铁路段i,在所有的行程中被经过多少次。然后,对于每个段i来说,如果使用IC卡的总费用是否比每次买纸质票更便宜。这里可能需要比较两种情况的总费用:买卡的总成本(C_i + 次数*B_i)和不买卡的总成本(次数*A_i)。取其中较小的那个。

对于求每段铁路经过多少次就可以用差分啦。

🖥️code:

#include <iostream>

using namespace std;

const int N = 1e5 + 10;
int n, m;
int f[N];
int pf[N];

int main()
{
	cin >> n >> m;
	for(int i = 1; i <= m; i++)
	{
		cin >> pf[i];
	}
	
	for(int i = 1; i < m; i++)
	{
		int x = pf[i], y = pf[i + 1];
		if(x > y) swap(x, y);
		f[x]++; f[y]--;
	}
	
	for(int i = 1; i < n; i++) f[i] = f[i - 1] + f[i];
	
	long long ans = 0;
	for(int i = 1; i < n; i++)
	{
		long long a, b, c; cin >> a >> b >> c;
		long long cost1 = a * f[i];
		long long cost2 = c + b * f[i];
		ans += min(cost1, cost2);
	}
	
	cout << ans << endl;
	
	return 0;
} 

2.4 二维差分模板

【模板】二维差分

【解题】:

二维差分其实可以根据一维差分的意义推出来:

🖥️code:

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1000 + 10;
// 原数组 
LL a[N][N];
// 差分数组 
LL f[N][N];
int q, n, m;

int main()
{
	cin >> n >> m >> q;
	
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= m; j++)
		{
			cin >> a[i][j];
			// 二维差分 
			f[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
		}
	}
	
	while(q--)
	{
		int x1, y1, x2, y2, k;
		cin >> x1 >> y1 >> x2 >> y2 >> k;
		f[x1][y1] += k;
		f[x2 + 1][y1] -= k;
		f[x1][y2 + 1] -= k;
		f[x2 + 1][y2 + 1] += k;
	}
	
	// 前缀和
	for(int i = 1; i <= n; i++) 
	{
		for(int j = 1; j <= m; j++)
		{
			f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];
			cout << f[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

2.5 二维差分例题

P3397 地毯 - 洛谷

【解题】:直接二维差分即可。

🖥️code:

#include <iostream>

using namespace std;

const int N = 1010;
int f[N][N];

int n, m;

int main()
{
	cin >> n >> m;
	
	// 二维差分 
	while(m--)
	{
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;
		f[x1][y1]++;
		f[x2 + 1][y1]--;
		f[x1][y2 + 1]--;
		f[x2 + 1][y2 + 1]++;
	}
	
	// 前缀和
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= n; j++)
		{
			f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];
			cout << f[i][j] << " ";
		}
		cout << endl;
	 } 
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值