区间维护

差分常用性质:对一个区间进行增加/减少

1.糖糖别胡说,我真的不是签到题目
题目:
从前,有n只萌萌的糖糖,他们分成了两组一起玩游戏。他们会排成一排,第i只糖糖会随机得到一个能力值bi。从第i秒的时候,第i只糖糖就可以消灭掉所有排在他前面的和他不是同一组的且能力值小于他的糖糖。

为了使游戏更加有趣,糖糖的爸爸,娇姐,会发功m次,第i次发功的时间为ci,则在第ci秒结束后,b1,b2,.....,bci都会增加1.

现在,娇姐想知道在第n秒后,会有多少只糖糖存活下来。

输入描述:
第一行只有一个整数T(T<6),表示测试数据的组数。
第二行有两个整数n,m。表示糖糖的个数以及娇姐发功的次数。(1<=n<=50000,1<=m<=1000000)
第三行到n+2行,每行有两个整数ai,bi,表示第i只糖糖属于那一组以及他的能力值。(0<=ai<=1,1<=bi<=1000000)
第n+3行到第n+m+2行,每行有一个整数ci,表示GTW第i次发功的时间.(1<=ci<=n)
输出描述:
总共T行,第i行表示第i组数据中,糖糖存活的个数。
输入
1
4 3
0 3
1 2
0 3
1 1
1
3
4

输出
3

思路:差分 + 前缀和 + 思维.
题解:对m次发功后成员所增加的能力值进行操作(利用差分)。

  1. 暴力比较. 数据水了点,可以直接枚举完所有可能进行前后比较,只要后面比当前不同组的成员能力值大就可以消灭掉。这种写法假如后面成员能力值均比前面小(最差时间复杂度达到O(n^2)).
  2. 可以从后往前遍历,每次更新两组最大成员体力值,若访问到当前成员比另一组目前最大成员体力值小则被消灭.

代码一

#include<iostream>
#include<string.h>
using namespace std;
typedef long long ll;
const int N =  5e4 + 5;
ll a[N], b[N], delt[N];
ll t, n, m;
int main()
{
    ios::sync_with_stdio(false);
    cin >> t;
    while(t --)
    {
        memset(a, 0, sizeof(a));
        memset(b, 0, sizeof(b));
        memset(delt, 0, sizeof(delt));
        cin >> n >> m;
        for(int i = 1; i <= n; i++)
        {
            cin >> a[i] >> b[i];
            delt[i] = b[i] - b[i-1];
        }
        for(int i = 1; i <= m; i++)
        {
            int c;
            cin >> c;
            delt[1] ++;
            delt[c+1] --;
        }
        ll res = n;
        for(int i = 1; i <= n; i++)
            b[i] = b[i-1] + delt[i];
        for(int i = 1; i <= n; i++)
        {
            for(int j = i + 1; j <= n; j++)
            {
                if(b[j] > b[i] && a[j] != a[i])
                {
                    res --;
                    break;
                }
            }
        }
        cout << res << endl;
    }
    return 0;
}

代码二

#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N =  5e4 + 5;
ll a[N], b[N], delt[N];
ll t, n, m;
int main()
{
    ios::sync_with_stdio(false);
    cin >> t;
    while(t --)
    {
        memset(a, 0, sizeof(a));
        memset(b, 0, sizeof(b));
        memset(delt, 0, sizeof(delt));
        cin >> n >> m;
        for(int i = 1; i <= n; i++)
        {
            cin >> a[i] >> b[i];
            delt[i] = b[i] - b[i-1];
        }
        for(int i = 1; i <= m; i++)
        {
            int c;
            cin >> c;
            delt[1] ++;
            delt[c+1] --;
        }
        ll res = n;
        for(int i = 1; i <= n; i++)
            b[i] = b[i-1] + delt[i];
        ll max0 = -1, max1 = -1;
        for(int i = n; i >= 1; i--) //从后往前更新两组组员最大值并进行消灭(只要另一组最大值比当前不同组的成员值大则可消灭)
        {
            if(a[i]) //组别为1
            {
                max1 = max(max1, b[i]); //更新1组最大值
                if(max0 > b[i]) res --; //若0组最大值比当前值大则该组员被消灭
            }
            else //组别为0
            {
                max0 = max(max0, b[i]); //更新0组最大值
                if(max1 > b[i]) res --;
            }
        }
        cout << res << endl;
    }
    return 0;
}


2.值周
题目:
JC内长度为L的马路上有一些值周同学,每两个相邻的同学之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…L,都有一个值周同学。 由于水宝宝有用一些区间来和ssy搞事情,所以为了避免这种事走漏风声,水宝宝要踹走一些区域的人。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的人(包括区域端点处的两个人)赶走。你的任务是计算将这些人都赶走后,马路上还有多少个人。
输入描述:
第一行有2个整数L和M,L代表马路的长度,M代表区域的数目,L和M之间用一个空格隔开。 接下来的M行每行包含2个不同的整数,用一个空格隔开,表示一个区域的起始点和终止点的坐标
输出描述:
1个整数,表示马路上剩余的人的数目。

输入
500 3
150 300
100 200
470 471

输出
298

说明
对于所有的数据,1≤L≤100000000

对于10%的数据,1<=M<=100

对于20%的数据,1<=M<=1000

对于50%的数据,1<=M<=100000

对于100%的数据,1<=M<=1000000

思路:差分 + 前缀和 + 思维.
题解:初始化设每个点人数为1,接着m次区间维护,最后计算前缀和时判断一下若该点上人数<=0均证明该点的人已被除掉,否则该点有一人。
代码:

#include<iostream>
#include<algorithm>
using namespace std;
int n, m;
const int N = 1e8 + 5;
int a[N];
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    n ++;
    fill(a, a + 1 + n, 0);
    a[1] = 1; //初始化每一个点上为一人
    for(int i = 1; i <= m; i++)
    {
        int l, r;
        cin >> l >> r;
        l ++, r ++;
        a[l] --;
        a[r+1] ++;
    }
    int res = 0;
    for(int i = 1; i <= n; i++)
    {
        a[i] += a[i-1];
        if(a[i] == 1) res ++;
    }
    cout << res << endl;
    return 0;
}

正解:离散化 + 贪心.
利于贪心思想将起点进行升序排序,接着分三种线段覆盖情况进行讨论,并对不重复的区间的人员进行清除,最后计算剩下的人员即可,此处离散化思想仅仅是把需要操作的区间存入数组中,若套模板把数据范围缩小后进行差分操作无法满足实际每一个点代表一个人存在,会把实际计算结果改变.
ps:特别要注意区间下标从0开始.
代码:

#include<iostream>
#include<algorithm>
#include<utility>
using namespace std;
typedef pair<int, int> PII;
int n, m;
const int N = 1e6 + 5;
PII a[N];
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int l, r;
        cin >> l >> r;
        a[i] = make_pair(l, r);
    }
    sort(a + 1, a + 1 + m);
    int l = a[1].first, r = a[1].second, sum = r - l + 1;
    for(int i = 2; i <= m; i++)
    {
        //三种清除区域可能
        if(a[i].second <= r) continue;
        if(a[i].first > r)
        {
            l = a[i].first;
            r = a[i].second;
            sum += r - l + 1;
        }
        else
        {
            sum += a[i].second - r;
            r = a[i].second;
        }
    }
    cout << n - sum + 1 << endl; //+1:下标从0开始
    return 0;
}

3.火烧赤壁
题目:
给定每个起火部分的起点和终点,请你求出燃烧位置的长度之和。

输入格式
第一行一个整数,表示起火的信息条数 n。
接下来 n 行,每行两个整数a,b,表示一个着火位置的起点和终点。

输出格式
输出一行一个整数表示答案。

输入
3
-1 1
5 11
2 9
输出
11

说明/提示
数据规模与约定
对于全部的测试点,保证1≤n≤2×10^4, −231≤a≤b<231,且答案小于2^31。

思路:贪心 + 区间维护.
题解:这题太迷,相当于运用上题离散化思想,一样分三种区间覆盖情况对区间进行维护即可(情况图在文末最后)
代码:

#include<iostream>
#include<algorithm>
#include<utility>
#define debug(x) cout << "***" << x << endl
using namespace std;
typedef long long ll;
typedef pair<ll, ll> PII;
const int N = 2e4 + 5;
PII a[N];
int n;
int main()
{
    ios::sync_with_stdio(false);
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        int l, r;
        cin >> l >> r;
        a[i] = make_pair(l, r);
    }
    sort(a + 1, a + 1 + n); //先first升序后second升序
    ll l = a[1].first, r = a[1].second, res = r - l;
    for(int i = 2; i <= n; i++)
    {
        //线段覆盖共三种情况
        if(a[i].second <=  r) continue;
        if(a[i].first > r)
        {
            l = a[i].first;
            r = a[i].second;
            res += (r - l);
        }
        else
        {
            res += (a[i].second - r);
            r = a[i].second;
        }
    }
    cout << res<< endl;
    return 0;
}

离散化模板思路:将原先的区间缩小,并利用unique去重,升序排序后进行有效区间的差分,最后利用前缀和计算出该起始端点是否为有效区间.
unique是c++自带的一个函数,表示对一个数列去重,然后返回不重复的元素个数(减去首地址).
官话笔记:离散化,就是当我们只关心数据的大小关系时,用排名代替原数据进行处理的一种预处理方法.
如:[4, 7, 9] 离散化后:[1, 2, 3]
代码:

#include<iostream>
#include<algorithm>
#include<string.h>
#define debug(x) cout << "***" << x << endl;
using namespace std;
int n;
const int N = 2e4 + 5;
int a[N], b[N], t[N*2], delt[N*2];
int main()
{
    ios::sync_with_stdio(false);
    memset(delt, 0, sizeof(delt));
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i] >> b[i];
        t[i*2-1] = a[i]; //奇数
        t[i*2] = b[i]; //偶数
    }
    sort(t + 1, t + 1 + n + n); //为后期二分而排序(unique函数只检测相邻的元素,最好用前先排序)
    int len = unique(t + 1, t + 1 + n + n) - t - 1; //将原数据从1开始划分等级(去重.可无序)
    for(int i = 1; i <= n; i++)
    {
        a[i] = lower_bound(t + 1, t + len + 1, a[i]) - t; //二分查找a[i]所在的rank
        b[i] = lower_bound(t + 1, t + len + 1, b[i]) - t;
        //将a[i] -> b[i-1]进行+1, 可使b[i]作为端点无效
        delt[a[i]] ++;
        delt[b[i]] --;
    }
    int res = 0;
    for(int i = 1; i <= len; i++)
    {
        delt[i] += delt[i-1];
        if(delt[i] > 0) //端点有效
            res += t[i+1] - t[i];
    }
    cout << res << endl;
    return 0;
}

posted @ 2021-01-07 10:26  ~K2MnO4  阅读(176)  评论(0)    收藏  举报