差分数组 学习笔记

我想想我第一次接触差分数组算法,是在洛谷上找树状数组线段树题的时候,遇到的 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二维代码写法样例,受益匪浅,不得不说这就是练习量的差异(好好看,好好学)
**qhjjddw**


这算是学习数据结构过程中的一个小插曲,差分也让我对于前缀和有了更深的理解,但是想来不过就是一个简单的技巧而已,记一下记一下……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值