我想想我第一次接触差分数组算法,是在洛谷上找树状数组线段树题的时候,遇到的 P1083 借教室。看到题解里有一篇号称更好理解也更好实现的算法,那我为什么不学?
差分数组
首先要说的是,差分数组的思想与前缀和的思想是密不可分的,不如说差分数组就是前缀和数组的逆运算
我们平时是这么思考前缀和数组的?
用sum[i]来存储前i个数的和,然后用sum[r]-sum[l-1]来表示 l ~ r 之间所有数的和。(l-1原因是 l ~ r 只看要包含l)而sum数组便可以通过简单的递推求出来
//代码核心
for(int i=1;i<=n;i++)
{cin>>a[i];sum[i]=sum[i-1]+a[i];}
for(int i=1;i<=q;i++)
{cin>>l>>r;cout<<sum[r]-sum[l-1]<<" ";}
而差分是如何思考的呢??
我们给定前i个数相邻两个数的差(1<=i<=n),求每一项a[i](1<=i<=n)。
此时无非就是用作差的方式求得每一项,此时我们可以有一个作差数组diff,diff[i]用于记录a[i]-a[i-1],然后对于每一项a[i],我们可以递推出来
for(int i=1;i<=n;i++)
{cin>>diff[i];a[i]=diff[i]+a[i-1];}
for(int i=1;i<=n;i++)
{cout<<a[i];}
简单来说,前缀和是用元数据求元与元之间的并集关系,而差分则是根据元与元之间的逻辑关系求元数据,是互逆思想。
以上有借鉴于dalao对 P1083 借教室 的题解
如果数据有给出左端点和右端点的处理方式,则在两个端点分别处理,最后靠递推能线性处理求出区间内每个点的状态。
bool check(int x) //本题因需要二分查找优化,所以差分写在bool函数里判断
{
memset(diff, 0, sizeof(diff)); //每次都要初始化diff数组
FOR(i, 1, x)
{
diff[l[i]] += d[i]; //左端点起始,表示之后的每个点都要+d[i]
diff[r[i]+1] -= d[i]; //右端点终止,之后每个点-d[i],这样就表示出来区间内的改变量
}
FOR(i, 1, n)
{
need[i] = need[i-1] + diff[i]; //线性递推可知总状态
if(need[i] > rest[i]) //判断
return false;
}
return true; //如果始终没能false跳出,就return true
}
本题的主函数思路也变得明确
int main()
{
n = read();
m = read();
FOR(i, 1, n) rest[i] = read();
FOR(i, 1, m)
{
d[i] = read();
l[i] = read();
r[i] = read();
}
//前面都是读入
int start=1, ending=m;
if(check(m)) //如果用最后的m点判断都是true,则说明完全ok,打印结果结束程序
{
cout<<"0";
exit(0);
}
while(start < ending) //没能完全ok就二分查找找最早是哪一个出了问题
{
int mid = start + (ending - start) / 2;
if(check(mid))
start = mid + 1;
else
ending = mid;
}
printf("-1\n%d", ending);
return 0;
}
//因为如果前一份订单都不满足,那么之后的所有订单都不用继续考虑;
//而如果后一份订单都满足,那么之前的所有订单一定都可以满足,符合局部舍弃性,
//所以可以二分订单数量。
初见,学!
我第二次遇到可以用差分的题则是 P2184 贪婪大陆,现在想来越来越觉得差分和前缀和是密不可分的,这题就是更新并查询,线段覆盖范围内有多少种不同的线段叠加。
怎么做呢?
更新:我们统计左右端点的前缀和,意思是每次更新单点前面有几个左端点有几个右端点(求前缀和)
查询:因为若查询线段左边有右端点,则那个右端点代表的线段就不在区间内;同理,若查询线段右边有左端点,则左端点代表线段就不在区间内。于是用总线段数量减去上面两个查询结果就是总个数。
是不是?根据元和元间的逻辑关系推导出全貌,这便是差分的妙处,而统计这里统计差分也是用到了线段树或者树状数组求前缀和(我用的是结构体挂两棵线段树的做法)
至于代码……(那个时候我撸的树太丑了就不贴了,懂个意思就行,关键在于差分思想)
终于到正题了,为什么想到写差分的博客,是胡队昨天给我分享的一道中山大学的校赛题 Monitor ,问的是矩阵范围内的方块覆盖,那必然是差分了,统计边界条件前缀和,然后最后查询再看能不能全覆盖。
但是二维……蒟蒻表示瑟瑟发抖,这可怎么写啊……
但是思想是没错的,于是我参阅了胡队的代码(好好看,好好学)
#define id(i,j) (i - 1) * m + (j - 1)
//就是从这步,把二维压成一维,爽到
bool check (int a, int b, int c, int d) {
int ret = 0;
ret += sum[id(c,d)]; //统计矩形内的标记过的点
if (a > 1) ret -= sum[id(a-1,d)];
if (b > 1) ret -= sum[id(c,b-1)];
if (a > 1 && b > 1) ret += sum[id(a-1,b-1)];
return ret == (d - b + 1) * (c - a + 1); //如果和矩形大小一样,则说明是全标记,返回1;如果不……
}
int main (void)
{
while (~scanf("%d%d", &n, &m)) {
memset(sum,0,sizeof(sum));
p = read(); //开始更新
while (p--) {
a=read(); b=read(); c=read(); d=read();
sum[id(a,b)]++;
if (c != n) sum[id(c+1,b)]--;
if (d != m) sum[id(a,d+1)]--;
if (c != n && d != m) sum[id(c+1,d+1)]++;
}
FOR(i, 1, n)
FOR(j, 1, m)
{
if (i > 1) sum[id(i,j)] += sum[id(i-1,j)]; //利用一开始的边界元条件更新出全部点位的前缀和
if (j > 1) sum[id(i,j)] += sum[id(i,j-1)];
if (i > 1 && j > 1) sum[id(i,j)] -= sum[id(i-1,j-1)];
}
FOR(i, 1, n)
FOR(j, 1 ,m)
sum[id(i,j)] = (sum[id(i,j)] != 0); //把矩形内所有更新过的点标记为1,方便合并和统计个数
FOR(i, 1 ,n)
FOR(j, 1, m)
{
if (i > 1) sum[id(i,j)] += sum[id(i-1,j)];
if (j > 1) sum[id(i,j)] += sum[id(i,j-1)];
if (i > 1 && j > 1) sum[id(i,j)] -= sum[id(i-1,j-1)];
//从此之后数组种sum[id(, )]一个点表达的就是前缀总和
}
q = read(); //开始查询
while (q--) {
a=read(); b=read(); c=read(); d=read();
puts(check(a,b,c,d) ? "YES" : "NO");
}
}
}
差分处理端点→→更新全部点位→→点位清为1→→统计前缀和→→查询并打印结果
爽到,巩固差分思想plus二维代码写法样例,受益匪浅,不得不说这就是练习量的差异(好好看,好好学)
这算是学习数据结构过程中的一个小插曲,差分也让我对于前缀和有了更深的理解,但是想来不过就是一个简单的技巧而已,记一下记一下……