分治专题

在牛子老师的博客下边看到 yspm 给了 CF1019E。看了一眼,不会。看了题解,我超边分治 + 闵可夫斯基和,一个都不会。乐。

还有 20 天,还能补多少坑呢,不好说。

仍然是每天高压作业。但是出乎意料的晚上不是很失眠,虽然说醒了以后还是很困。

现象:让大象出现的事物或者方法。大象是一种体量很大、包容万物的生物,平时喜欢躲在泥土下面拿长长的鼻子冒充仙人掌以攫食长颈鹿。因为大象的鼻子看起来像是罕见的仙人掌,很多人希望能将新奇的仙人掌品种给引入园艺界。此时大象就会拔地而起,狠揍植物猎人并把植物猎人挂在树梢上当做大雁发表演说用的讲台。

同理可定义抽象:一种让大象出现的方法。即用真空泵将大象从地下抽出。

P6932 [ICPC2017 WF] Money for Nothing

洛谷题面翻译什么寄吧。题意是每一天都能倒卖一次,但是自始至终只能和一对交易。也就是左下角卖家右上角买家的矩形最大面积。

首先可能成为答案的点肯定是从左上到右下一排。然后固定左下的点,右上的点的选取有单调性,于是可以按着决策单调性的分治写法直接冲。

忘了怎么写了,贺了一发。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
#define int long long
using namespace std;
int n,m,ans,cnta,cntb;
struct node{
    int x,y;
    bool operator<(const node&s)const{
        return x==s.x?y<s.y:x<s.x;
    }
}a[500010],b[500010],tmp[500010];
void solve(int l,int r,int L,int R){
    if(l>r||L>R)return;
    int mid=(l+r)>>1,pos=0,val=-0x3f3f3f3f3f3f3f3f;
    for(int i=L;i<=R;i++){
        if(a[mid].x<b[i].x||a[mid].y<b[i].y){
            int ret=(b[i].x-a[mid].x)*(b[i].y-a[mid].y);
            if(ret>val)val=ret,pos=i;
        }
    }
    ans=max(ans,val);
    if(pos){
        solve(l,mid-1,L,pos);solve(mid+1,r,pos,R);
    }
}
signed main(){
    scanf("%lld%lld",&m,&n);
    for(int i=1;i<=m;i++)scanf("%lld%lld",&tmp[i].x,&tmp[i].y);
    sort(tmp+1,tmp+m+1);a[0].y=0x3f3f3f3f3f3f3f3f;
    for(int i=1;i<=m;i++){
        if(a[cnta].y>tmp[i].y)a[++cnta]=tmp[i];
    }
    for(int i=1;i<=n;i++)scanf("%lld%lld",&tmp[i].x,&tmp[i].y);
    sort(tmp+1,tmp+n+1);
    for(int i=1;i<=n;i++){
        while(cntb&&b[cntb].y<tmp[i].y)cntb--;
        b[++cntb]=tmp[i];
    }
    solve(1,cnta,1,cntb);
    printf("%lld\n",ans);
    return 0;
}

gym103202L Forged in the Barrens

十分奇妙。

首先看上去就有个最大流建模。模拟最大流大概也能过。但我们不这么干,因为有点丑陋。

每个区间一头的贡献是 \(+1\),一头是 \(-1\)。那么考虑一个 dp:设 \(dp_{0/1/2,0/1/2,l,r,x}\) 为区间 \([l,r]\) 选了 \(x\) 段,左右没有东西 / 空了一段正贡献 / 负贡献。则转移考虑分治,有:

\[dp_{s_l,s_r,l,r,x}=\max(\max_{x_1+x_2=x}dp_{s_l,0,l,mid,x_1}+dp_{0,s_r,mid+1,r,x_2},\\ \max_{x_1+x_2+1=x}dp_{s_l,1,l,mid,x_1}+dp_{2,s_r,mid+1,r,x_2},\\ \max_{x_1+x_2+1=x}dp_{s_l,2,l,mid,x_1}+dp_{1,s_r,mid+1,r,x_2},\\ dp_{s_l,s_r,l,mid,x},dp_{s_l,s_r,mid+1,r,x})\]

发现这玩意关于 \(x\) 是凸的,于是直接闵可夫斯基和合并凸包。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#define int long long
int n,a[200010];
const int inf=0x3f3f3f3f3f3f3f3f;
struct node{
    vector<int>v[3][3];
};
vector<int>Merge(vector<int>a,vector<int>b){
    for(int i=a.size()-1;i>=1;i--)a[i]-=a[i-1];
    for(int i=b.size()-1;i>=1;i--)b[i]-=b[i-1];
    vector<int>c(a.size()+b.size()-1);
    c[0]=a[0]+b[0];
    merge(a.begin()+1,a.end(),b.begin()+1,b.end(),c.begin()+1,greater<int>());
    for(int i=1;i<a.size()+b.size()-1;i++)c[i]+=c[i-1];
    return c;
}
vector<int>max(vector<int>a,vector<int>b){
    int n=max(a.size(),b.size());
    while(a.size()<n)a.push_back(-inf);
    while(b.size()<n)b.push_back(-inf);
    for(int i=0;i<n;i++)a[i]=max(a[i],b[i]);
    return a;
}
node solve(int l,int r){
    if(l==r){
        node ans;
        ans.v[0][0].push_back(0);ans.v[0][0].push_back(0);
        ans.v[1][0].push_back(-inf);ans.v[1][0].push_back(a[l]);
        ans.v[2][0].push_back(-a[l]);
        ans.v[0][1].push_back(-inf);ans.v[0][1].push_back(a[l]);
        ans.v[1][1].push_back(-inf);
        ans.v[2][1].push_back(-inf);
        ans.v[0][2].push_back(-a[l]);
        ans.v[1][2].push_back(-inf);
        ans.v[2][2].push_back(-inf);
        return ans;
    }
    int mid=(l+r)>>1;
    node L=solve(l,mid),R=solve(mid+1,r),ans;
    for(int l=0;l<3;l++){
        for(int r=0;r<3;r++){
            ans.v[l][r]=max(max(Merge(L.v[l][0],R.v[0][r]),Merge(L.v[l][1],R.v[2][r])),max(Merge(L.v[l][2],R.v[1][r]),max(L.v[l][r],R.v[l][r])));
        }
    }
    return ans;
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
    node ans=solve(1,n);
    for(int i=1;i<=n;i++)printf("%lld\n",ans.v[0][0][i]);
    return 0;
}

uoj#592. 新年的聚会

比较神奇。

考虑随机打乱所有点,然后维护若干个独立集。每次找一个 \(x\),对每个独立集分治找到和它们连的所有边,然后加入最小的独立集里边。复杂度不会证明。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <random>
#include <ctime>
#include <vector>
#include "meeting.h"
using namespace std;
int n,m,cnt,id[1010];
vector<pair<int,int> >ans;
vector<int>v[2010];
mt19937 rnd(time(0)^(unsigned long long)(new char));
bool check(int pos,int x,int l,int r){
    vector<int>ret;ret.push_back(x);
    for(int i=l;i<=r;i++)ret.push_back(v[pos][i]);
    return meeting(ret);
}
bool solve(int pos,int x,int l,int r,bool jud){
    if(l>r)return false;
    if(!jud&&check(pos,x,l,r))return false;
    if(l==r){
        ans.push_back(make_pair(x,v[pos][l]));
        return true;
    }
    int mid=(l+r)>>1;
    if(!check(pos,x,l,mid)){
        solve(pos,x,l,mid,true);solve(pos,x,mid+1,r,false);
    }
    else solve(pos,x,mid+1,r,true);
    return true;
}
vector<pair<int,int> >solve(int n){
    for(int i=0;i<n;i++)id[i]=i;
    shuffle(id,id+n,rnd);
    for(int i=0;i<n;i++){
        int x=id[i],pos=-1;
        for(int j=1;j<=cnt;j++){
            if(!solve(j,x,0,v[j].size()-1,false)){
                if(pos==-1||v[j].size()<v[pos].size())pos=j;
            }
        }
        if(pos==-1)pos=++cnt;
        v[pos].push_back(x);
    }
    return ans;
}

CF1442D Sum

大结论题。

结论是过程中最多只有一个序列被选了一部分。

证明:反证,假设有两个序列 \(a,b\) 选到了 \(a_x,b_y\),其中 \(a_x\ge b_y\),那么选到 \(a_{x+1},b_{y-1}\) 一定更优。

于是就相当于做一个强制某个元素不选的背包,直接分治就好了。

轻微卡常(也可能是我实现有问题),每个分治节点开个 dp 会 T。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <vector>
#define int long long
using namespace std;
int n,k,ans,sum[3010],cnt[3010],dp[3010][21];
vector<int>a[3010];
void solve(int l,int r,int dep){
    if(l==r){
        int sum=0,cnt=0;
        ans=max(ans,dp[k][dep]);
        for(int x:a[l]){
            cnt++;sum+=x;
            if(cnt>k)break;
            ans=max(ans,dp[k-cnt][dep]+sum);
        }
        return;
    }
    int mid=(l+r)>>1;
    for(int i=0;i<=k;i++)dp[i][dep+1]=dp[i][dep];
    for(int i=mid+1;i<=r;i++){
        for(int j=k;j>=cnt[i];j--)dp[j][dep+1]=max(dp[j][dep+1],dp[j-cnt[i]][dep+1]+sum[i]);
    }
    solve(l,mid,dep+1);
    for(int i=0;i<=k;i++)dp[i][dep+1]=dp[i][dep];
    for(int i=l;i<=mid;i++){
        for(int j=k;j>=cnt[i];j--)dp[j][dep+1]=max(dp[j][dep+1],dp[j-cnt[i]][dep+1]+sum[i]);
    }
    solve(mid+1,r,dep+1);
}
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%lld",&cnt[i]);
        for(int j=1;j<=cnt[i];j++){
            int x;scanf("%lld",&x);a[i].push_back(x);sum[i]+=x;
        }
    }
    for(int i=1;i<=k;i++)dp[i][0]=-0x3f3f3f3f3f3f3f3f;
    solve(1,n,0);
    printf("%lld\n",ans);
    return 0;
}

CF1553H XOR and Distance

参考 x义x 老师的神秘解法。

\(dp_{x,d}\) 为强制 \(x\) 使用和 \(x\) 相比只有低 \(d\) 位不同的元素的答案,\(mn{x,d}\) 为它们和 \(x\) 异或最小值,\(mx_{x,d}\) 为最大值。

考虑转移到 \(d+1\)\(mn,mx\) 的转移比较显然:设 \(x,y\) 满足 \(y=x+2^d\),则

\[mn_{x,d+1}=\min(mn_{x,d},mn_{y,d}+2^d) \]

\(mx\) 的式子是一样的。那么考虑转移 \(dp\)。元素对有三类:

  1. 都在 \((x,d)\) 中:就是 \(dp_{x,d}\)
  2. 都在 \((y,d)\) 中:就是 \(dp_{y,d}\)
  3. 两个都在:这个可以发现是 \(mn_{x,d}-mx_{y,d}+2^d\)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n,k,dp[1<<19],mx[1<<19],mn[1<<19];
int main(){
    scanf("%d%d",&n,&k);
    memset(dp,0x3f,sizeof(dp));memset(mn,0x3f,sizeof(mn));memset(mx,-0x3f,sizeof(mx));
    for(int i=1;i<=n;i++){
        int x;scanf("%d",&x);
        mx[x]=mn[x]=0;
    }
    for(int i=0;i<k;i++){
        for(int s=0;s<(1<<k);s++){
            if(!((s>>i)&1))continue;
            dp[s^(1<<i)]=dp[s]=min(dp[s^(1<<i)],dp[s]);
            dp[s^(1<<i)]=min(dp[s^(1<<i)],mn[s]-mx[s^(1<<i)]+(1<<i));
            dp[s]=min(dp[s],mn[s^(1<<i)]-mx[s]+(1<<i));
            int mn1=mn[s^(1<<i)],mn2=mn[s],mx1=mx[s^(1<<i)],mx2=mx[s];
            mn[s^(1<<i)]=min(mn[s^(1<<i)],mn2+(1<<i));
            mn[s]=min(mn[s],mn1+(1<<i));
            mx[s^(1<<i)]=max(mx[s^(1<<i)],mx2+(1<<i));
            mx[s]=max(mx[s],mx1+(1<<i));
        }
    }
    for(int i=0;i<(1<<k);i++)printf("%d ",dp[i]);puts("");
    return 0;
}

CF1179E Alesya and Discrete Math

这分治题单是没有贪心是吧。

首先都分治题单了肯定要分治是不是。于是考虑分治:找到每个函数取值为 \(\dfrac L2\) 的位置,然后排序,找到中间的一个,分到左右两半为子问题。询问次数 \(n\log n\log L\),过不去。

分治不好去掉,但是似乎没必要询问每个的值,只要找到最中间的一个函数是谁就行了。随机一个函数检查,然后每次在多的那一半里边重复该过程,那么期望 \(O(\log n)\) 次找到。这样单层的询问次数就从 \(O(n\log L)\) 变成了 \(\sum\log n\log L\),总复杂度是 \(\sum_{i=1}^{\log n}i2^{\log n-i}\log L=O(n\log L)\),可以通过。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define int long long
using namespace std;
int n,L;
pair<int,int>ans[1010];
int query(int id,int x){
    printf("? %lld %lld\n",id,x);
    fflush(stdout);
    scanf("%lld",&x);
    return x;
}
int getpos(int l,int r,int val,int id){
    while(l<r){
        int mid=(l+r)>>1;
        if(query(id,mid)>=val)r=mid;
        else l=mid+1;
    }
    return l;
}
int find(int l,int r,int val,int cnt,vector<int>v){
    int pos=v[rand()%v.size()];
    int p=getpos(l,r,val,pos);
    vector<int>L,R,ret;ret.push_back(pos);
    for(int x:v){
        if(x==pos)continue;
        int tmp=query(x,p);
        if(tmp<val)R.push_back(x);
        if(tmp>val)L.push_back(x);
        if(tmp==val)ret.push_back(x);
    }
    while(!ret.empty()&&L.size()<cnt)L.push_back(ret.back()),ret.pop_back();
    while(!ret.empty())R.push_back(ret.back()),ret.pop_back();
    if(L.size()==cnt)return p;
    if(L.size()>cnt)return find(l,r,val,cnt,L);
    else return find(l,r,val,cnt-L.size(),R);
}
void solve(int l,int r,int L,int R,vector<int> v){
    if(v.empty())return;
    if(v.size()==1){
        ans[v[0]].first=L,ans[v[0]].second=R;
        return;
    }
    int mid=l+(r-l)/v.size()*(v.size()>>1);
    int pos=find(L,R,mid,v.size()>>1,v);
    vector<int>ll,rr,ret;
    for(int x:v){
        int tmp=query(x,pos);
        if(tmp>mid)ll.push_back(x);
        if(tmp<mid)rr.push_back(x);
        if(tmp==mid)ret.push_back(x);
    }
    while(!ret.empty()&&ll.size()<(v.size()>>1))ll.push_back(ret.back()),ret.pop_back();
    while(!ret.empty())rr.push_back(ret.back()),ret.pop_back();
    solve(l,mid,L,pos,ll);
    solve(mid,r,pos,R,rr);
}
signed main(){
    scanf("%lld%lld",&n,&L);
    vector<int>v;
    for(int i=1;i<=n;i++)v.push_back(i);
    solve(0,L,0,1e18,v);
    puts("!");
    for(int i=1;i<=n;i++)printf("%lld %lld\n",ans[i].first,ans[i].second);
    return 0;
}

CF865F Egg Roulette

官方题解没看懂。

首先有个神秘结论:对于任意包含不超过 \(R-1\)\(A\)\(B\) 的前缀可以随意重排而不改变不公平度。官方题解说因为该赢的不会输该输的不会赢。感觉很感性。称这样的前缀是合法的。

然后既然这样了就有个比较显然的东西:任意序列都可以提 \(R-1\)\(A\)\(R-1\)\(B\) 放开头而不改变不公平度。

考虑先算出来不公平度,这一点通过不断往最后边塞操作,直到塞到一个合法前缀来得到。首先两个人的操作数是相等的,然后若现在 Alice 有 \(cnta\) 个操作,Bob 有 \(cntb\) 个操作,则若 Alice 往最后边放一个,Bob 增加的胜场数是 Alice 往最后放一个 R 的方案,即 \(\dbinom{cnta-1}{R-1}\dbinom{cntb}R\)

这部分的复杂度是 \(2^{2C}\) 的,但是可以加个剪枝:先找到一个足够小的不公平度,然后通过最优化剪枝减小状态数。这个可以通过哪边小补哪里的贪心方式填出一个不公平度。

然后是统计答案。同样往最后边塞操作,当塞到一个合法前缀的时候统计答案。假设这时有 \(cnta\)\(A\)\(cntb\)\(B\),而这个前缀有 \(a\) 个确定的 \(A\)\(b\) 个确定的 \(B\),那方案数显然是 \(\dbinom{cnta-a+cntb-b}{cnta-a}\)。记搜即可通过。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <map>
#include <vector>
#define int long long
using namespace std;
int n,m,ans,C[70][70],suma[70],sumb[70];
char s[70];
void dfs(int x,int y,int cnta,int cntb){
    if(ans<=1||abs(x-y)-C[cnta][n]*C[cntb][n]>ans)return;
    if(cnta<n||cntb<n){
        ans=min(ans,abs(x-y));return;
    }
    if(cnta>=n)dfs(x,y+C[cnta-1][n-1]*C[cntb][n],cnta-1,cntb);
    if(cntb>=n)dfs(x+C[cntb-1][n-1]*C[cnta][n],y,cnta,cntb-1);
}
map<int,int>mp[70][70];
int solve(int x,int y,int cnta,int cntb){
    if(mp[cnta][cntb].find(x-y)!=mp[cnta][cntb].end())return mp[cnta][cntb][x-y];
    if(abs(x-y)-C[cnta][n]*C[cntb][n]>ans)return mp[cnta][cntb][x-y]=0;
    if(cnta<n||cntb<n){
        if(cnta>=suma[cnta+cntb]&&cntb>=sumb[cnta+cntb])return mp[cnta][cntb][x-y]=C[cnta+cntb-suma[cnta+cntb]-sumb[cnta+cntb]][cnta-suma[cnta+cntb]];
        else return mp[cnta][cntb][x-y]=0;
    }
    int ans=0;
    if(s[cnta+cntb]!='B')ans+=solve(x,y+C[cnta-1][n-1]*C[cntb][n],cnta-1,cntb);
    if(s[cnta+cntb]!='A')ans+=solve(x+C[cntb-1][n-1]*C[cnta][n],y,cnta,cntb-1);
    return mp[cnta][cntb][x-y]=ans;
}
signed main(){
    scanf("%lld%lld%s",&n,&m,s+1);
    for(int i=0;i<=(n+m<<1);i++){
        C[i][0]=1;
        for(int j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
    }
    int x=0,y=0,cnta=n+m,cntb=n+m;
    while(cnta>=n&&cntb>=n){
        if(x>y)y+=C[cnta-1][n-1]*C[cntb][n],cnta--;
        else x+=C[cntb-1][n-1]*C[cnta][n],cntb--;
    }
    ans=abs(x-y);
    dfs(0,0,n+m,n+m);
    for(int i=1;i<=(n+m<<1);i++){
        suma[i]=suma[i-1]+(s[i]=='A');
        sumb[i]=sumb[i-1]+(s[i]=='B');
    }
    printf("%lld\n",solve(0,0,n+m,n+m));
    return 0;
}
posted @ 2023-07-02 21:40  gtm1514  阅读(22)  评论(0编辑  收藏  举报