【ACM】【Misc】离线算法

1. 莫队算法

特点

  • O2优化
  • 离线询问,进行排序与分块,对每个块集中处理
  • 已知一个区间的答案,推出所有区间的答案
  • 时间复杂度 O ( n n ) O(n\sqrt{n}) O(nn )

适用问题

  • 给一个区间,多次查询(O(n^2)),允许离线
  • 答案从(i,j)转换到(i-1,j)、(i,j-1)、(i+1,j)、(i,j+1)的复杂度是O(1)
  • 即:已知一个区间的答案,求下一个区间的答案时,可直接根据所在数据 a [ i ] a[i] a[i]处理更新

普通莫队

  • 将i分块,在每个块内,从1到n扫j。
/*
 * 莫队模板题
 * 一个数组,N(5e4)个数字,M(5e4)次查询:区间中抽两个数字,两者相等的概率。
 * 先上暴力做法
 */
#include <bits/stdc++.h>
using namespace std;
int n,m,maxn;
typedef long long ll;
const int N = 5e4+5;
int a[N],cnt[N];
ll sum;
struct node{
    int l,r,id;
    bool operator < (const node &b) const{
        if(l/maxn!=b.l/maxn) return l<b.l;
        return r<b.r;
    }
}q[N];

void add(int i){
    sum += cnt[i];
    cnt[i]++;
}

void del(int i){
    cnt[i]--;
    sum -= cnt[i];
}

ll gcd(ll a,ll b){
    return b?gcd(b,a%b):a;
}
ll ans1[N],ans2[N];

int main(){
    scanf("%d%d",&n,&m);
    maxn = sqrt(n);
    for(int i=1;i<=n;++i) scanf("%d",&a[i]);
    for(int i=0;i<m;++i) scanf("%d%d",&q[i].l,&q[i].r),q[i].id = i;
//    下面是暴力做法
//    for(int i=1;i<=m;++i) {
//        memset(cnt,0,sizeof(cnt));
//        int fz = 0,fm = (q[i].r-q[i].l+1)*(q[i].r-q[i].l)/2;
//        if(fm==0) {puts("0/1");continue;}
//        for(int j = q[i].l;j<=q[i].r;++j){
//            fz += cnt[a[j]];
//            cnt[a[j]]++;
//        }
//        if(fz==0) {puts("0/1");continue;}
//        int g = gcd(fz,fm);
//        printf("%d/%d\n",fz/g,fm/g);
//    }
    // 下面是莫队算法
    sort(q,q+m);
    for(int i=0,l=1,r=0;i<m;++i){
        if(q[i].l==q[i].r){ans1[q[i].id]=0,ans2[q[i].id]=1;continue;}
        while(l>q[i].l) add(a[--l]);
        while(r<q[i].r) add(a[++r]);
        while(l<q[i].l) del(a[l++]);
        while(r>q[i].r) del(a[r--]);
        ans1[q[i].id] = sum;
        ans2[q[i].id] = (ll)(r-l+1)*(r-l)/2;
    }
    for(int i=0;i<m;++i){
        if(ans1[i]!=0){
            ll g = gcd(ans1[i],ans2[i]);
            ans1[i] /= g, ans2[i] /= g;
        }
        else ans2[i] = 1;
        printf("%lld/%lld\n",ans1[i],ans2[i]);
    }
}

一个比较快的板子

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int maxn = 3e5 + 233;
set<int> st;
unordered_map<int, int> ump;
int block, n, m, a[maxn], ans[maxn], cnt, p[maxn], num[maxn];
struct qs
{
    int l, r, idx;
} b[maxn];
bool cmp(const qs &x, const qs &y)
{
    return (x.l / block) == (y.l / block) ? x.r < y.r : x.l < y.l;
}
inline void pls(int x)
{
    num[p[a[x]]++]--;
    int t = p[a[x]];
    num[t]++;
    if (t > cnt)
        cnt = t;
}
inline void sbs(int x)
{
    num[p[a[x]]--]--;
    int t = p[a[x]];
    num[t]++;
    while (!num[cnt])
        cnt--;
}
int main()
{
    cin >> n >> m;
    block = n / sqrt(m * 2 / 3);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]), st.insert(a[i]);
    for (int i = 1; i <= m; i++)
    {
        scanf("%d%d", &b[i].l, &b[i].r);
        b[i].idx = i;
    }
    int tot = 0;
    for (auto it = st.begin(); it != st.end(); it++)
    {
        ump[*it] = ++tot;
    }
    for (int i = 1; i <= n; i++)
        a[i] = ump[a[i]];
    stable_sort(b + 1, b + 1 + m, cmp);
    int l = 0, r = 0;
    for (int i = 1; i <= m; i++)
    {
        while (b[i].l < l)
            pls(--l);
        while (b[i].l > l)
            sbs(l++);
        while (b[i].r < r)
            sbs(r--);
        while (b[i].r > r)
            pls(++r);
        if(cnt <= (b[i].r - b[i].l + 1) / 2) ans[b[i].idx] = 1;
        else ans[b[i].idx] = cnt*2 - (b[i].r - b[i].l + 1);
    }
    for (int i = 1; i <= m; i++){
        printf("%d\n", ans[i]);
    }
}

带修改莫队

JNUOJ1228 486的区间
带修改莫队的特殊之处,就在于多了一个特征:时间。
将i和j分块,对每个i块,再对每个j块,从1~n扫t。

/*
 * 题意:n个数,每个数为ai,m个询问,求:l到r区间ai*num(ai)的和
 * 带修莫队模板
 */
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+6;
int n,m,a[N],sqn,qcnt,ccnt,num[N*10];
long long ans[N],sm;
 
struct query{
    int l,r,id,t;
    bool operator <(const query &b)const{
        if(l/sqn!=b.l/sqn) return l/sqn<b.l/sqn;
        if(r/sqn!=b.r/sqn) return r/sqn<b.r/sqn;
        return t<b.t;
    }
}q[N];
 
struct modify{
    int pos,pre,nex;
}c[N];
 
template <typename _Tp>
inline void IN(_Tp& dig){
    char c;dig = 0;
    while(c=getchar(),!isdigit(c));
    while(isdigit(c)) dig = dig * 10 + c -'0',c = getchar();
}
 
void add(int p){
    sm-=(long long)num[a[p]]*num[a[p]]*a[p];
    sm+=(long long)(++num[a[p]])*num[a[p]]*a[p];
}
 
void del(int p){
    sm-=(long long)num[a[p]]*num[a[p]]*a[p];
    sm+=(long long)(--num[a[p]])*num[a[p]]*a[p];
}
 
int main(){
    //freopen("input.txt","r",stdin);
    IN(n);IN(m);sqn = ceil(pow(n,(double)2/(double)3));
    for(int i=1;i<=n;++i) IN(a[i]);
    for(int i=1,x,y;i<=m;++i) {
        char str[4];
        if(scanf("%s",str),IN(x),IN(y),str[0]=='Q')
            q[++qcnt].l = x,q[qcnt].r = y,q[qcnt].id = qcnt,q[qcnt].t = ccnt;
        else
            c[++ccnt].pos = x,c[ccnt].nex = y,c[ccnt].pre = a[x],a[x] = y;
    }
    sort(q+1,q+1+qcnt);
    for(int i=ccnt;i>=1;--i) a[c[i].pos] = c[i].pre;
    for(int i=1,l=1,r=0,t=0;i<=qcnt;++i){
        while(l>q[i].l) add(--l);
        while(r<q[i].r) add(++r);
        while(l<q[i].l) del(l++);
        while(r>q[i].r) del(r--);
        while(t<q[i].t){
            int p = c[++t].pos;
            if(l<=p&&r>=p) del(p);
            a[p] = c[t].nex;
            if(l<=p&&r>=p) add(p);
        }
        while(t>q[i].t){
            int p = c[t].pos;
            if(l<=p&&r>=p) del(p);
            a[p] = c[t].pre;
            if(l<=p&&r>=p) add(p);
            t--;
        }
        ans[q[i].id] = sm;
    }
    for(int i=1;i<=qcnt;++i) printf("%lld\n",ans[i]);
}
 
/**************************************************************
    Problem: 1228
    User: IcecreamArtist
    Language: C++
    Result: Accepted
    Time:36 ms
    Memory:2696 kb
****************************************************************/

2. CDQ分治

三维偏序

二维偏序可先以其中一个特征排序,再用树状数组对第二个特性进行统计,累计答案。
二维偏序也可以用分治来做,就不需要套BIT了,见用归并排序求逆序对。(逆序对也可以用树状数组来做,BIT和分治本质上都是降维)
三维偏序则在分治的基础上加多了一维,则是分治上用树状数组处理第二个特性。
用第一个特征将数组排序,然后对这个区间进行分治。每到一个区间(l,r),都对(l,mid)和(mid+1,r)内部再以第二个特征进行排序。然后利用左区间,进行右区间的贡献累计(也可能是利用右区间,对左区间进行贡献累计,取决于第一维特征)。这个累计过程使用BIT,道理类似于归并排序求逆序对。记得BIT用完过后要进行撤销。

洛谷3157 动态逆序对

/*
 * CDQ分治模板题:三维偏序
 * 题意:给一个数组(1e5),m(5e4)次操作,每次删除指定数字,每次删除之前输出逆序对个数。
 * 思路:每次删除指定数字,等价于时间反向,每次加入指定数字。
 * 对于一个数字的加入,该时间点增加的逆序对数等于比他早加入、位置在他之前、比他大的数+比他早加入、位置在他之后、比他小的数。
 * 分治过程中用左区间对右区间进行贡献即可。
 */
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+6;
typedef long long ll;
int n,m,c[maxn],p[maxn];
struct data{int t,p,v;}a[maxn],b[maxn];
ll ans[maxn];
inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();return x*f;}
inline void insert(int x,int y){for(;x<=n;x+=(x&(-x))) c[x]+=y;}
inline int query(int x){int sm = 0;for(;x;x-=(x&(-x))) sm+=c[x];return sm;}

void cdq(int l,int r){
    if(l>=r) return;
    int mid = (l+r)>>1,tmp,ll=l-1,rr=mid;
    for(int i=l;i<=r;++i){
        if(a[i].t<=mid) b[++ll] = a[i];
        else b[++rr] = a[i];
    }
    // 将数组重新排序:mid左时间都早于右边;同时,左之中的数字按照位置从前往后排序,右之中的数字也是按位置从前往后排序
    for(int i=l;i<=r;++i) a[i]=b[i];
    tmp=l;
    for(int i=mid+1;i<=r;++i){
        // 每一个右边的,都要计算上左边中出现在他之前且比他大的
        for(;tmp<=mid&&a[tmp].p<a[i].p;tmp++) insert(a[tmp].v,1);
        ans[a[i].t]+=tmp-l-query(a[i].v);
    }
    for(int i=l;i<tmp;++i) insert(a[i].v,-1); // 撤销BIT
    tmp=mid;
    for(int i=r;i>=mid+1;--i){
        // 每一个右边的,还要计算上左边中出现在他之后且比他小的
        for(;tmp>=l&&a[tmp].p>a[i].p;tmp--) insert(a[tmp].v,1);
        ans[a[i].t]+=query(a[i].v-1);
    }
    for(int i=tmp+1;i<=mid;++i) insert(a[i].v,-1); // 撤销BIT
    cdq(l,mid);cdq(mid+1,r);
}

int main(){
    n=read(),m=read();
    int time = n,x;
    for(int i=1;i<=n;++i) a[i].v=read(),a[i].p=i,p[a[i].v]=i;
    for(int i=1;i<=m;++i) x=read(),a[p[x]].t=time--;
    for(int i=1;i<=n;++i) if(!a[i].t) a[i].t=time--;
    cdq(1,n);
    // 依次删除每个数,等价于反向加入每个数
    for(int i=1;i<=n;++i) ans[i]+=ans[i-1]; // 反向加入每个数,到第几次的结果。
    for(int i=n;i>=n-m+1;--i) printf("%lld\n",ans[i]);
}

优化1D/1D DP

洛谷2487 [SDOI2011]拦截导弹
该题目要计算最优决策的数量,以及每个元素在最优决策中出现了多少次。因此跑两遍dp。

//
// Created by artist on 2021/8/4.
//


#include<bits/stdc++.h>

using namespace std;
typedef long long ll;

//-----------------------------------------------------------------IO Template
namespace StandardIO {
    template<typename T>
    inline void read(T &x) {
        x = 0;
        T f = 1;
        char c = getchar();
        for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = -1;
        for (; c >= '0' && c <= '9'; c = getchar()) x = x * 10 + c - '0';
        x *= f;
    }

    template<typename T>
    inline void write(T x) {
        if (x < 0) putchar('-'), x *= -1;
        if (x >= 10) write(x / 10);
        putchar(x % 10 + '0');
    }
}
using namespace StandardIO;
//-----------------------------------------------------------------IO Template
const int maxn = 5e4+4;
#define mkp make_pair
#define fir first
#define sec second
#define lowbit(x) (x&-x)
#define pii pair<int,double>
pii f[maxn],g[maxn];// 统计答案及最优方案数
inline pii operator + (pii a,pii b) {return mkp(a.fir+b.fir,a.sec+b.sec);}
pii max(pii a,pii b){
    if(a.fir!=b.fir) return a.fir>b.fir?a:b;
    return mkp(a.fir,a.sec+b.sec);
}
int n,num;// num为离散化后

struct Node{
    int h,v,id;
}ori[maxn],tmp[maxn];

bool cmp(Node a,Node b){
    return a.h>b.h;
}

// 查找在他前面比他大的数目(反过来的树状数组)
struct BIT{
    pii tr[maxn];
    void add(int x,pii p){
        while(x>0){
            tr[x] = max(tr[x],p);
            x -= lowbit(x);
        }
    }
    pii query(int x){
        pii ans=mkp(0,0);
        while(x<=num){
            ans = max(ans,tr[x]);
            x += lowbit(x);
        }
        return ans;
    }
    void clear(int x){
        while(x>0){
            tr[x] = mkp(0,0);
            x -= lowbit(x);
        }
    }
}tr1;

inline void cdq1(int l,int r){
    if(l==r) return;
    int mid=(l+r)>>1;
    cdq1(l,mid);
    for(int i=l;i<=r;++i) tmp[i]=ori[i];
    sort(tmp+l,tmp+1+mid,cmp);
    sort(tmp+mid+1,tmp+r+1,cmp);
    // two pointers
    for(int i=l,j=mid+1;j<=r;++j){
        while(i<=mid&&tmp[i].h>=tmp[j].h) tr1.add(tmp[i].v,f[tmp[i].id]), i++;
        f[tmp[j].id] = max(f[tmp[j].id],tr1.query(tmp[j].v) + mkp(1,0));
    }
    for(int i=l;i<=mid;++i) tr1.clear(tmp[i].v);
    cdq1(mid+1,r);
}

bool cmp2(Node a,Node b){
    return a.h<b.h;
}

// 查找在他前面比他小的数目
struct BIT2{
    pii tr[maxn];
    void add(int x,pii p){
        while(x<=num){
            tr[x]=max(tr[x],p);
            x += lowbit(x);
        }
    }
    pii query(int x){
        pii ans=mkp(0,0);
        while(x>0){
            ans=max(tr[x],ans);
            x -= lowbit(x);
        }
        return ans;
    }
    void clear(int x){
        while(x<=num){
            tr[x]=mkp(0,0);
            x += lowbit(x);
        }
    }
}tr2;

// 此处要反转
inline void cdq2(int l,int r){
    if(l==r) return;
    int mid=(l+r)>>1;
    cdq2(l,mid);
    for(int i=l;i<=r;++i) tmp[i]=ori[i];
    sort(tmp+l,tmp+1+mid,cmp2);
    sort(tmp+mid+1,tmp+1+r,cmp2);
    for(int i=l,j=mid+1;j<=r;++j){
        while(i<=mid&&tmp[i].h<=tmp[j].h) tr2.add(tmp[i].v,g[tmp[i].id]),i++;
        g[tmp[j].id] = max(g[tmp[j].id],tr2.query(tmp[j].v)+mkp(1,0));
    }
    for(int i=l;i<=mid;++i) tr2.clear(tmp[i].v);
    cdq2(mid+1,r);
}

// 离散化
int V[maxn];
inline void discretize(){
    sort(V+1,V+1+n);
    num = unique(V+1,V+1+n)-V-1;
    for(int i=1;i<=n;++i) ori[i].v=lower_bound(V+1,V+1+num,ori[i].v)-V;
}

signed main() {
    // 输入
    read(n);
    for(int i=1;i<=n;++i) {
        read(ori[i].h),read(ori[i].v);
        V[i]=ori[i].v;
        ori[i].id=i;
    }
    discretize();
    for(int i=1;i<=n;++i) f[i]=g[i]=mkp(1,1); // initialize
    cdq1(1,n);
    reverse(ori+1,ori+1+n);  // dp from back to front
    cdq2(1,n);

    // 处理答案&输出
    pii ans=mkp(0,0);
    for(int i=1;i<=n;++i) ans=max(ans,f[i]);

    write(ans.fir);putchar(10);
    for(int i=1;i<=n;++i){
        if(f[i].fir+g[i].fir-1==ans.fir) printf("%.5lf%c",1.0*f[i].sec*g[i].sec/ans.sec,i==n?'\n':' ');
        else printf("%.5lf%c",0.0,i==n?'\n':' ');
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值