Loading

整体二分 学习笔记

整体二分

整体二分是一种离线算法,它可以以较少的码量来完成一些主席树的操作。

运用较广,拓展性较高。

大致思想

整体二分基于一种分治的思想,我的理解它就是一种二分和分治的结合,运用分治去搜索,利用二分来优化判断。

主席树例题为例。

首先考虑单个二分如何解决。

可将区间 \(l\)\(r\) 进行排序,二分查找。

复杂度 \(O(nm\log_{n})\)

妥妥的超时,接下来考虑整体二分。

顾名思义,我们可以将所有的询问进行离线,统一进行二分。

0.初始化

由于我们需要将询问离线,这里的离线是要将序列和询问都离线。

什么叫做把序列离线呢?

我们可以把序列的初始化,看作是一种修改。

这样理解起来就不会特别困难。

为了减小常数,我们可以现进行离散化。

struct edge
{
    int x , y , k , id , type;
}e[maxn * 2];

for(int i = 1;i <= n;i++)
    b[i].first = c[i] = a[i] = read() , b[i].second = i;

sort(b + 1 , b + n + 1);

for(int i = 1;i <= n;i++) a[b[i].second] = i;
//离散化

for(int i = 1;i <= n;i++) e[++cnt] = (edge){a[i] , 1 , 1 , i , 1};

for(int i = 1;i <= m;i++)
{
    e[++cnt].x = read() , e[cnt].y = read();
    e[cnt].k = read() , e[cnt].type = 2 , e[cnt].id = i;
}
//type为1,表示修改,2为查询。
//修改的x存的为值,id为位置。
//查询的x,y为左右边界,k为第几小,id为询问编号。

1.调用

由于整体二分是二分,所以必然需要一个左边界和一个右边界。

solve(1 , n , 1 , cnt);

但可以看到,调用入口运用了两个左右边界。

它的含义为,值域的左右边界为 \(1 - n\),操作的左右边界为 \(1 - cnt\)

2.二分实现

既然已经有了左右边界,那么我们就可以根据这一点来实现判断合法。

我们考虑将所有值小于 \(mid\) 的修改全部丢到一个数据结构来进行维护。

进行单点修改,区间查询的数据结构。

这里我们采用的是常数小的树状数组。

我们可以思考一下,数据结构里维护的是什么,是区间内小于 \(mid\) 的数的个数。

那么我们分治的内容是什么,不就是去查询小于 \(mid\) 的数是不是有 \(k\) 个吗。

根据二分的正确性,我们发现,最后二分出的结果,一定会是一个序列中的数。

有没有一种恍然大雾的感觉。

inline void solve(int l , int r , int ql , int qr)
{
    if(ql > qr) return;
    
    if(l == r)
    {
        for(int i = ql;i <= qr;i++)
            if(e[i].type == 2) ans[e[i].id] = l;
        //记录答案
        return;
    }

    int mid = (l + r) >> 1;
    int cnt1 = 0 , cnt2 = 0;
    
    for(int i = ql;i <= qr;i++)
    {
        if(e[i].type == 1)
        {
            if(e[i].x <= mid) a1[++cnt1] = e[i] , t.update(e[i].id , 1);
            else a2[++cnt2] = e[i];
            //修改树状数组
        }
        
        if(e[i].type == 2)
        {
            
            int res = t.ask(e[i].y) - t.ask(e[i].x - 1);
            if(res >= e[i].k) a1[++cnt1] = e[i];
            if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
            //注意,如果询问到它小于k,那么k要减去res,这一点和主席树是一样的。
        }
    }
    
    for(int i = ql;i <= qr;i++)
        if(e[i].type == 1 && e[i].x <= mid) t.update(e[i].id , -1);
    //清除当前的修改

    for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
    for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
    //更新序列
    
    solve(l , mid , ql , ql + cnt1 - 1);
    solve(mid + 1 , r , ql + cnt1 , qr);
}

以上,我们就可以过掉模板题了。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
int n , m , cnt;
int ans[maxn] ,  a[maxn] , c[maxn];
pair<int , int> b[maxn];

struct edge
{
    int x , y , k , id , type;
}e[maxn * 2] , a1[maxn * 2] , a2[maxn * 2];

inline int read()
{
    int asd = 0, qwe = 1; char zxc;
    while (!isdigit(zxc = getchar())) if (zxc == '-') qwe = -1;
    while (isdigit(zxc)) asd = asd * 10 + zxc - '0', zxc = getchar();
    return asd * qwe;
}

inline void solve(int l , int r , int ql , int qr);

int main()
{
    n = read() , m = read();
    
    for(int i = 1;i <= n;i++)
        b[i].first = c[i] = a[i] = read() , b[i].second = i;
    
    sort(b + 1 , b + n + 1);
    
    for(int i = 1;i <= n;i++) a[b[i].second] = i;
    for(int i = 1;i <= n;i++) e[++cnt] = (edge){a[i] , 1 , 1 , i , 1};

    for(int i = 1;i <= m;i++)
    {
        e[++cnt].x = read() , e[cnt].y = read();
        e[cnt].k = read() , e[cnt].type = 2 , e[cnt].id = i;
    }
    // return 0;
    solve(1 , n , 1 , cnt);

    for(int i = 1;i <= m;i++)
        printf("%d\n" , c[b[ans[i]].second]);
    return 0;
}

struct Tree
{
    int sum[maxn];
    
    inline int lowbit(int x) { return x & (-x); }
    inline void update(int x , int y) { while(x <= n) sum[x] += y , x += lowbit(x); }
    inline int ask(int x) { int res = 0; while(x) res += sum[x] , x -= lowbit(x); return res; }

}t;

inline void solve(int l , int r , int ql , int qr)
{
    if(ql > qr) return;
    
    if(l == r)
    {
        for(int i = ql;i <= qr;i++)
            if(e[i].type == 2) ans[e[i].id] = l;
        return;
    }

    int mid = (l + r) >> 1;
    int cnt1 = 0 , cnt2 = 0;
    
    for(int i = ql;i <= qr;i++)
    {
        if(e[i].type == 1)
        {
            if(e[i].x <= mid) a1[++cnt1] = e[i] , t.update(e[i].id , 1);
            else a2[++cnt2] = e[i];
        }
        
        if(e[i].type == 2)
        {
            
            int res = t.ask(e[i].y) - t.ask(e[i].x - 1);
            if(res >= e[i].k) a1[++cnt1] = e[i];
            if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
        }
    }
    
    for(int i = ql;i <= qr;i++)
        if(e[i].type == 1 && e[i].x <= mid) t.update(e[i].id , -1);

    for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
    for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
    
    solve(l , mid , ql , ql + cnt1 - 1);
    solve(mid + 1 , r , ql + cnt1 , qr);
}

它甚至比我的主席树还快。

关于时间复杂度

写的时候,有没有这样一种感觉。

明明感觉会T,但最后快到飞起。

我们可以用我们良好的初赛素养,来证明一下时间复杂度。

由于程序主体是递归。

我们可以设:

\[f(n) = 2 \times f(\dfrac{n}{2} ) + n\log_{n} \]

\(n\log(n)\) 为修改时间复杂度,我们姑且认为,每次递归,它递归了一半。

设:

\[2^t=n \]

那么有:

\[f(2^t)=2 \times f(2^{t-1})+2^tt \]

\[f(2^t)=2 \times (2 \times f(2^{t-2})+2^{t-1}(t-1))+2^tt \]

\[f(2^t)=2 \times (2 \times f(2^{t-2}))+2^t(t+(t-1)) \]

\[f(2^t)=2 \times (2 \times f(2 \times f(2^{t-3})+2^{t-2}(t-2))+2^t(t+(t-1)) \]

可知:

\[f(2^t)=2 \times (2 \times 2 \times f(2^{t-3}))+2^t(t+(t-1)+(t-2)) \]

那么:

\[f(2^t)=2^t \times \sum_{i=1}^ti \]

即:

\[f(2^t)=2^t \times \dfrac{t \times (t - 1)}{2} \]

省掉常数:

\[f(2^t)=2^t t^2 \]

带回 \(2^t=n\)

\[f(n)=n(\log_{n})^2 \]

所以,我们就较为严谨的证明了它的复杂度,为 \(O(n(\log_{n})^2)\)

一些例题

P3527 [POI2011] MET-Meteors

一道很经典的整体二分模板题。

我们还是考虑以一种二分答案的方式去进行解决。

题意

每次给区间加上一个值,问几次以后,一些位置的和可以大于 \(k\), 如果不能,输出NIE。

应该是比较模板的一道题。

思路

二分多少次以后,一些位置的和可以大于 \(k\),如果当前已经大于,就丢到左边,不然就丢到右边。

Code

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
const int inf = 1e15;
const int maxn = 300010;
int n , m , k , now , o[maxn] , p[maxn] , ans[maxn];
int lc[maxn] , rc[maxn] , a[maxn] , c[maxn] , c1[maxn] , c2[maxn];
vector<int> hav[maxn];

inline int read()
{
    int asd = 0 , qwe = 1; char zxc;
    while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
    while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
    return asd * qwe;
}

namespace Tree
{
    struct TREE
    {
        int sum[maxn];

        inline int lowbit(int x) { return x & (-x); }
        inline void add(int x , int y) { for(int i = x;i <= m;i += lowbit(i)) sum[i] += y; }
        inline int ask(int x) { int res = 0; for(int i = x;i;i -= lowbit(i)) res += sum[i]; return res; }
    }t;

    inline void update(int l , int r , int x)
    {
        if(l <= r)
            t.add(l , x) , t.add(r + 1 , -x);
        else
            t.add(l , x) , t.add(m + 1 , -x),
            t.add(1 , x) , t.add(r + 1 , -x);
    }

    inline int ask(int x)
    {
        return t.ask(x);
    }
}

using namespace Tree;
inline void solve(int l , int r , int ls , int rs);

signed main()
{
    n = read() , m = read();
    for(int i = 1;i <= n;i++) c[i] = i;
    for(int i = 1;i <= m;i++) o[i] = read();
    for(int i = 1;i <= n;i++) p[i] = read();
    for(int i = 1;i <= m;i++) hav[o[i]].push_back(i);
    k = read();
    for(int i = 1;i <= k;i++)
        lc[i] = read() , rc[i] = read() , a[i] = read();
    k++ , lc[k] = 1 , rc[k] = m , a[k] = inf;
    solve(1 , k , 1 , n);
    for(int i = 1;i <= n;i++)
    {
        if(ans[i] == k) puts("NIE");
        else printf("%lld\n" , ans[i]);
    }
    return 0;
}

inline void solve(int l , int r , int ls , int rs)
{
    if(l == r || ls > rs)
    {
        for(int i = ls;i <= rs;i++) ans[c[i]] = l;
        return;
    }

    int mid = (l + r) >> 1 , cnt1 = 0 , cnt2 = 0;
    while(now < mid) now++ , update(lc[now] , rc[now] , a[now]);
    while(now > mid) update(lc[now] , rc[now] , -a[now]) , now--;

    for(int i = ls;i <= rs;i++)
    {
        int sum = 0;
        for(auto j : hav[c[i]]) sum += ask(j);
        if(sum >= p[c[i]]) c1[++cnt1] = c[i];
        if(sum < p[c[i]]) c2[++cnt2] = c[i];    
    }

    for(int i = 1;i <= cnt1;i++) c[ls + i - 1] = c1[i];
    for(int i = 1;i <= cnt2;i++) c[ls + cnt1 + i - 1] = c2[i];

    solve(l , mid , ls , ls + cnt1 - 1);
    solve(mid + 1 , r , ls + cnt1 , rs);
}

P2617 Dynamic Rankings

同样的例题,带修区间第 \(k\) 大。

由于我们在主席树板子里已经讲过了,将序列的初始化,看作修改,那么现在有了修改,我们同样可以看做是现删除曾经的,再加上现在的。

Code

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n , m , cnt , tot , a[maxn] , ans[maxn];

struct edge
{
    int x , y , k , id , type;
}e[maxn * 3] , a1[maxn * 3] , a2[maxn * 3];

inline int read()
{
    int asd = 0 , qwe = 1; char zxc;
    while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
    while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
    return asd * qwe;
}

namespace Tree
{
    struct TREE
    {
        int sum[maxn];

        inline int lowbit(int x) { return x & (-x); }
        inline void add(int x , int y) { for(int i = x;i <= n;i += lowbit(i)) sum[i] += y; }
        inline int ask(int x) { int res = 0; for(int i = x;i;i -= lowbit(i)) res += sum[i]; return res; }
    }t;

    inline int ask(int x) { return t.ask(x); }
    inline void update(int l , int x) { t.add(l , x); }
}

using namespace Tree;
inline void solve(int l , int r , int ql , int qr);

int main()
{
    n = read() , m = read();
    
    for(int i = 1;i <= n;i++) 
        a[i] = read() , e[++cnt] = (edge){a[i] , 0 , 1 , i , 1};
    
    for(int i = 1;i <= m;i++)
    {
        char ch; cin >> ch; int l = read() , r = read() , k = (ch == 'Q' ? read() : 0);
        if(ch == 'Q') e[++cnt] = (edge){l , r , k , ++tot , 2};
        if(ch == 'C') e[++cnt] = (edge){a[l] , 0 , -1 , l , 1} , e[++cnt] = (edge){r , 0 , 1 , l , 1} , a[l] = r;
    }   

    solve(0 , 1e9 , 1 , cnt);

    for(int i = 1;i <= tot;i++) cout << ans[i] << endl;
    return 0;
}

inline void solve(int l , int r , int ql , int qr)
{
    if(ql > qr) return;
    
    if(l == r)
    {
        for(int i = ql;i <= qr;i++)
            if(e[i].type == 2) ans[e[i].id] = l;
        return;
    }

    int mid = (l + r) >> 1;
    int cnt1 = 0 , cnt2 = 0;
    for(int i = ql;i <= qr;i++)
    {
        if(e[i].type == 1)
        {
            if(e[i].x <= mid) a1[++cnt1] = e[i] , update(e[i].id , e[i].k);
            else a2[++cnt2] = e[i];
        }
        
        if(e[i].type == 2)
        {
            int res = ask(e[i].y) - ask(e[i].x - 1);
            if(res >= e[i].k) a1[++cnt1] = e[i];
            if(res < e[i].k) a2[++cnt2] = e[i] , a2[cnt2].k -= res;
        }
    }

    for(int i = ql;i <= qr;i++)
        if(e[i].type == 1 && e[i].x <= mid) update(e[i].id , e[i].k * -1);

    for(int i = 1;i <= cnt1;i++) e[ql + i - 1] = a1[i];
    for(int i = 1;i <= cnt2;i++) e[ql + cnt1 + i - 1] = a2[i];
    
    solve(l , mid , ql , ql + cnt1 - 1);
    solve(mid + 1 , r , ql + cnt1 , qr);
}

P7424 [THUPC2017] 天天爱射击

这应该也是一道区间第 \(k\) 小的裸题吧。

思路

考虑整体二分,将所有的询问全部离线。

我们可以将每一个子弹赋值,值可以取他们射击的时间。

这样,被第 \(k\) 颗子弹击碎,就相当于查询第 \(k\) 小了。

这里与其它整体二分的不同的点是。

它的统计答案,要这样写。

if(lc > rc || l == r)
{
    for(int i = lc;i <= rc;i++)
        if(e[i].type == 2) ans[l]++;
    return ;
}

因为我们二分的是时间,而时间又和子弹的编号相等。

所以查询到这些板子会被这颗子弹击碎。

相当与这颗子弹击碎的数量增加。

一个细节

要注意,这里的板子并没有说一定会被击碎,所以初始化时,操作要多加一个。

这样,没有被击碎的板子二分到的就是,最后一颗子弹的下一颗子弹,而不是最后一颗子弹。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 300010;
int n , m , cnt , ans[maxn];

struct edge
{
    int x , y , k , id , type;
}e[maxn * 2] , a1[maxn * 2] , a2[maxn * 2];

inline int read()
{
    int asd = 0 , qwe = 1; char zxc;
    while(!isdigit(zxc = getchar())) if(zxc == '-') qwe = -1;
    while(isdigit(zxc)) asd = asd * 10 + zxc - '0' , zxc = getchar();
    return asd * qwe;
}

namespace TREE
{
    int l = 200010;
    int sum[maxn];

    inline int lowbit(int x) { return x & (-x); }
    inline void update(int x , int y) { while(x <= l) sum[x] += y , x += lowbit(x); }
    inline int ask(int x) { int res = 0; while(x) res += sum[x] , x -= lowbit(x); return res; }
}

using namespace TREE;
inline void solve(int l , int r , int lc , int rc);

signed main()
{
    n = read() , m = read();
    for(int i = 1;i <= n;i++)
        e[++cnt].x = read() , e[cnt].y = read() , e[cnt].k = read() , e[cnt].id = i , e[cnt].type = 2;
    for(int i = 1;i <= m;i++)
        e[++cnt].x = read() , e[cnt].y = i , e[cnt].type = 1;
    e[++cnt].x = 1 , e[cnt].y = ++m , e[cnt].type = 1;
    //多加一颗子弹。
    solve(1 , m , 1 , cnt);
    for(int i = 1;i < m;i++)
        cout << ans[i] << endl;
    return 0;
}

inline void solve(int l , int r , int lc , int rc)
{
    if(lc > rc || l == r)
    {
        for(int i = lc;i <= rc;i++)
            if(e[i].type == 2) ans[l]++;
        return ;
    }

    int mid = (l + r) >> 1 , cnt1 = 0 , cnt2 = 0;
    for(int i = lc;i <= rc;i++)
        if(e[i].type == 1)
        {
            if(e[i].y <= mid) update(e[i].x , 1) , a1[++cnt1] = e[i];
            else a2[++cnt2] = e[i];
        }
    for(int i = lc;i <= rc;i++)
        if(e[i].type == 2) 
        {
            int sum = ask(e[i].y) - ask(e[i].x - 1);
            if(sum >= e[i].k)  a1[++cnt1] = e[i];
            else a2[++cnt2] = e[i] , a2[cnt2].k -= sum;
        }
    
    for(int i = lc;i <= rc;i++)
        if(e[i].type == 1 && e[i].y <= mid)
            update(e[i].x , -1);
    for(int i = 1;i <= cnt1;i++) e[i + lc - 1] = a1[i];
    for(int i = 1;i <= cnt2;i++) e[i + lc + cnt1 - 1] = a2[i];

    solve(l , mid , lc , lc + cnt1 - 1);
    solve(mid + 1 , r , lc + cnt1 , rc);
}

posted @ 2021-11-13 18:01  JiaY19  阅读(55)  评论(0编辑  收藏  举报