[省选联考 2024] 题解
D1T1 P10217 季风
题意有点抽象,大概就是要求我们对两个有若干次重复的序列进行操作,每次可以将这两个序列都向上或向下调整一个值,但是调整的绝对值的总和有限制,问能否最终将总和调整至固定值。
由于 \(m\) 不一定是 \(n\) 的倍数,因此序列在重复若干次之后可能会遗留一些散块,这是不好处理的,我们考虑枚举这个散块的大小,这样就只用考虑整块的加减。
简化条件之后就可以开始求解了,先考虑其中一个序列,设散块的和为 \(suma\),大小为 \(l\),整块的和为 \(Asum\),则如果我们要选择 \(k\) 个散块,则我们需要调整的大小就是 \(|suma+k \cdot Asum -x|\),另一个序列同理,当二者和不超过 \(k \cdot n + l\) 时有解。
我们将两个绝对值函数求和并减去右边的一次函数,那我们要求的即为最小零点,注意到相加后的函数也是凸函数,直接两次二分的复杂度是 \(O(n\log n)\) 的,直接对折点分讨就可以做到线性,这里给个我的考场 90 分代码(赛时因为 abs 惨遭 CE)。
#include<bits/stdc++.h>
#define ll __int128
#define N 1000005
#define inf 0x7f7f7f7f7f7f7f7f
using namespace std;
ll read(){
    ll x=0,f=1;char ch=getchar();
    while(ch<'0' || ch>'9')f=(ch=='-'?-1:f),ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x*f;
}
void write(ll x){
    if(x<0)x=-x,putchar('-');
    if(x/10)write(x/10);
    putchar(x%10+'0');
}
ll a[N],b[N];
ll n,k,x,y,Asum,Bsum,suma=0,sumb;
ll labs(ll x){
	return (x<0?-x:x);
}
ll f(ll pos,ll ty){
    if(ty==1)return labs(suma+pos*Asum-x);
    else return labs(sumb+pos*Bsum-y);
}
bool check(ll mid){
    ll vala=(f(mid+1,1)-f(mid,1));
    ll valb=(f(mid+1,2)-f(mid,2));
    return vala+valb-n*k>0;
}
void solve(){
    n=read(),k=read(),x=read(),y=read();Asum=0,Bsum=0;ll ans=inf;
    for(ll i=1;i<=n;i++){
        a[i]=read();b[i]=read();
        Asum+=a[i];Bsum+=b[i];
    }
    if(x==0 && y==0){write(0);putchar('\n');return;}
    suma=0,sumb=0;
    for(ll i=1;i<=n;i++){
        suma+=a[i];sumb+=b[i];
        ll l=1,r=500000000000000ll;
        while(l<r){
            ll mid=(l+r)>>1;
            if(check(mid)>0)r=mid;
            else l=mid+1;
        }
        r=l;l=0;
        while(l<r){
            ll mid=(l+r)>>1;
            if(f(mid,1)+f(mid,2)<=(mid*n+i)*k)r=mid;
            else l=mid+1;
        }
        if(f(l,1)+f(l,2)<=(l*n+i)*k)ans=min(ans,l*n+i);
        if(i%10000==0)write(i);
    }
    if(ans==inf)write(-1),putchar('\n');
    else write(ans),putchar('\n');
}
int main(){
    ll T=read();while(T--)solve();
    return 0;
}
D1T2 P10218 魔法手杖
我们先考虑只有异或该怎么做,这类问题的一个传统套路是从高位到低位贪心检验前面确定的为加上这位为 1 是否有解,有解就将答案这位设成 1,否则不变并向下继续考虑,因此我们只用关心怎样解决如何判定答案的问题。
由于这个问题有良好的贪心性质,我们也从上往下依次进行判定,类似数位 dp 的操作,我们只考虑到当前位都贴紧答案上界的数,这样如果有些数已经在比较高的某些位置(我们已经考虑过的位置)大于答案,那我们就不用再在下面处理。这样说可能有些抽象,更具体地,若是关注判定过程中的其中一层,我们可以将其分为以下几种情况:
- 
若答案这位为 0,那么如果我们不在这位进行异或操作,这位为 1 的数字就可以不用考虑,直接递归处理这位为 0 的数字,进行操作的情况同理。
 - 
若答案这位为 1,如果这位既有 0 又有 1 那肯定无解,因为我们没办法只通过异或让所有数这位都是 1,否则操作唯一,递归处理即可。
 
这样我们就解决了全是异或操作的情况,但是由于这个判定过程是在 trie 上贪心,加上枚举复杂度将会达到 \(O(nk^2)\),不过我们其实可以直接考虑:若当前层颜色唯一,则这位答案可以为 1,否则按照对不对这位进行操作各递归两边进行计算,这样就能做到 \(O(nk)\)
接下来要引入加法,由于加法操作也有能够贪心的良好性质,我们尝试能不能不做大的改动,只在原本求解的框架上进行小的修改。
继续考察判定过程,上述第一种情况不变,第二种则由于加法操作的引进,不一定无解,此时无解条件就变成了为 0 的子树最小值加上操作已经确定的位和操作未确定的位都设成 1 之后的数(考虑最小的数必须要满足条件)必须要大于钦定答案这位为 1 后的值,且花费能够接受,如果合法则这位可以为 1。
但是这样会有一个问题:改成加法操作的子树也会对答案产生影响,我们必须对其进行递归,但这样两个子树的操作就不独立,必须一起考虑,但是我们的复杂度建立在拆分的基础上,这样会导致复杂度退化。
怎么解决呢?我们刚才提过,加法有良好的贪心性质,我们只需要考虑加法子树中最小的数,只有它会对答案产生影响,在递归过程中顺便记录现在已经钦定加法的子树最小值,判定的时候还要考虑这个值在最好情况下能不能满足条件,即可解决问题,剩下的按照异或的方法分类讨论即可。
#include<bits/stdc++.h>
#define ll __int128
#define inf 4e36
#define N 150005
using namespace std;
ll read(){
    ll x=0,f=1;char ch=getchar();
    while(ch<'0' || ch>'9')f=(ch=='-'?-1:f),ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x*f;
}
void write(ll x){
    if(x<0)x=-x,putchar('-');
    if(x/10)write(x/10);
    putchar(x%10+'0');
}
ll n,m;
ll a[N],tot=1,minn[N*121];
ll ans;
int ch[N*121][2];
long long cost[N*121],b[N];
ll maxc(ll x,ll y){
    return (x>y?x:y);
}
void solve(int p,ll k,ll minplus,ll res,long long rescos,ll opr){
    if(!p && minplus<inf)return ans=maxc(ans,minplus+(((__int128)1)<<(k+1))-1+opr),void();
    else if(!p)return;
    if(k==-1)return ans=maxc(ans,res),void();
    int ls=ch[p][0],rs=ch[p][1],flag=0;
    if(cost[ls]<=rescos && opr+min(minplus,minn[ls])+((__int128)1<<k)-1>=((__int128)1<<k)+res){
        solve(rs,k-1,min(minplus,minn[ls]),res|((__int128)1<<k),rescos-cost[ls],opr);
        flag=1;
    }
    if(cost[rs]<=rescos && opr+min(minplus,minn[rs])+((__int128)1<<k)-1>=res){
        solve(ls,k-1,min(minplus,minn[rs]),res|((__int128)1<<k),rescos-cost[rs],opr|((__int128)1<<k));
        flag=1;
    }
    if(flag)return;
    solve(ls,k-1,minplus,res,rescos,opr);
    solve(rs,k-1,minplus,res,rescos,opr|((__int128)1<<k));
}
void solve(){
    n=read(),m=read();ll k=read();ans=0;minn[0]=inf;
    for(ll i=1;i<=tot;i++)minn[i]=inf,cost[i]=ch[i][0]=ch[i][1]=0;tot=1;
    for(ll i=1;i<=n;i++)a[i]=read();
    for(ll i=1;i<=n;i++)b[i]=read();
    for(ll i=1;i<=n;i++){
        ll p=1;
        minn[p]=min(minn[p],a[i]);
        cost[p]+=b[i];
        for(ll j=k-1;j>=0;j--){
            ll it=((a[i]&((__int128)1<<j))?1:0);
            if(!ch[p][it])ch[p][it]=++tot;
            p=ch[p][it];minn[p]=min(minn[p],a[i]);
            cost[p]+=b[i];
        }
    }
    if(cost[1]<=m){write(minn[1]+((__int128)1<<k)-1);putchar('\n');return;}
    solve(1,k-1,inf,0,m,0);write(ans);putchar('\n');
}
int main(){
    memset(minn,0x3f,sizeof(minn));
    ll c=read(),T=read();while(T--)solve();
    return 0;
}
D1T3 P10219 虫洞
讲一些 @Kubic 的 这篇文章 中没有提到的点,做法原文已经解释得很详细了,这里补充了一些细节和完整实现。
对于两个连通块能够连边当且仅当它们同构的结论,这里补充一个感性理解,首先每个连通块的一个颜色的所有边都会连到另一个连通块上,否则一定不合法。
然后你可以将性质 4 这么理解:先考虑归纳,你现在将若干个连通块的边连成了环,现在这个环上的连通块各有一个分身,本体在你一开始所在连通块上,走连通块间的边相当于切换本体所在的连通块,否则就是所有分身走对应的一步。
这个性质要求切换本体所在连通块的顺序不影响答案,不难发先这其实就是同构的定义。
然后,在将转移式转化为生成函数时,原文写的比较简略:
初值是 \(F_1 = \frac{1}{1-x}\)。
接着,对于分式分解部分如何维护 \(w_{i,j}\) 以及求出答案,我们先假设已经将所有小于 \(i\) 的数进行了分式分解,那么 \(F_i\) 形似:
对于求和里的每一项进行分解,那就待定系数法解个方程就行。
无标号转为有标号只要乘 \((n-1)!\) 就行,原因在于如果直接 \(n!\),某个编号填到另外一个节点时会有有且仅有一种编号方式使这两种同构,因此还要除 \(n\)。
当 \(m=0\) 扩展时,原图只有同构的连通块才能互相连边,因此我们要用哈希判断一下同构,然后对于每一个类型的连通块分别计算,发现只要我们对点的出边按编号排序求出 dfs 序,那么同构当且仅当任意一条一个连通块有的边另一个连通块也有(边形如 \((u,v,w)\),\(u\) 和 \(v\) 表示原边端点 dfs 序,\(w\) 是编号)。
这里 dfs 树根随便选不难发现这样很对,严谨证明可以归纳。哈希判断一个方便的方法是,你对每条本质不同边随机赋权,连通块权值就是边异或和,map 判重即可。
缩点之后转移也要改,设当前算的连通块类型大小为 \(w\),那么转移就变成了:
原因在于此时大小实际上是 \(wi\),然后分式分解的时候注意一下系数就行。
最后放一下我的 完整实现,写的比较丑还不如平方快(
D2T1 P10220 迷宫守卫
题目要求我们在操作若干次后最大化 Bob 遍历的字典序,最大化字典序可以直接贪心,于是我们考虑每次能强迫 Bob 选的最大数是多少,即我们要求出强迫 Bob 走到某个数需要的最小花费,然后贪心进行选择,然后递归到路径上的其余子树再进行求解。
怎样求出这个花费呢?由于一个点向左向右的这个决策只会受到子树内的数的影响,我们从子树往上进行递推,假设我们现在已经求出了两个儿子走到各自子树中的叶子的最小花费,现在我们将两个信息进行合并,若当前我们要求的是走到左子树中某一个节点的最小花费,那现在有三种选择:
- 
直接唤醒当前节点的守卫,使得 Bob 只能先往左走。
 - 
若是右边子树中所有数都大于这个数,那我们不需要花费,Bob 就会直接往左子树走
 - 
我们还可以改变右边子树,使得 Bob 如果走到右边只能走到一个比这个节点大的节点,让他自己选择走到左子树中的这个节点,此时的花费就是强制选择一个右边比它大节点的花费最小值。
 
右边子树同理,只是没有第一种情况。这样就足够了吗?其实有一个问题:假如我们选择了第三种情况,花费会被重复计算两次,于是我们就需要在递归出这个子树后减掉这部分贡献使得其不被重复计算(这样的正确性是你递归到右半边时一定至少会先选择一个比你当前钦定的右边子树的节点编号大的节点)。
但是这又引出一个问题:刚才我们的操作相当于只让第三种情况的花费在这个子树中有效,但是一二种情况的操作是永久的,换句话说,你可能会考虑到第三种情况操作的暂时性而选择一个花费不是最小的节点(无论你选不选这个最小的节点字数内答案都一样,但是子树外答案就不一定一样了)。
这个问题其实也很好解决,只要先贪心选择最小满足子树内的贪心要求,再在回溯时考虑剩下的空间能不能让我在一开始的时候选择第三种操作满足子树外的贪心要求,用个 map 存一下就行,至此此题得到解决。
#include<bits/stdc++.h>
#define N 200005
#define ll long long
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;
ll read(){
    ll x=0,f=1;char ch=getchar();
    while(ch<'0' || ch>'9')f=(ch=='-'?-1:1),ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x*f;
}
void write(ll x){
    if(x<0)x=-x,putchar('-');
    if(x/10)write(x/10);
    putchar(x%10+'0');
}
ll n,k,tot;
ll ans[N],c[N],q[N],le[N],mar[N];
struct Node{
    ll c,w;
    Node(ll c=0,ll w=0):c(c),w(w){}
};
vector<Node> t[N];
map<ll,pair<ll,ll> > T[N]; 
void dfs(ll x){
    if(x>=(1<<n))return t[x].push_back(Node(0,q[x])),void();
    ll ls=(x<<1),rs=(x<<1)|1;dfs(ls);dfs(rs);
    ll lasls=inf,lasrs=inf,lpos=0,rpos=0,bel=0;Node val;
    for(ll i=1,lim=t[ls].size()+t[rs].size();i<=lim;i++){
        if(rpos==t[rs].size() || (lpos!=t[ls].size() && t[ls][lpos].w>t[rs][rpos].w))val=t[ls][lpos++],bel=0;
        else val=t[rs][rpos++],bel=1;
        if(bel==0){
            ll cos=inf;
            if(t[rs].size()!=0 && t[rs][t[rs].size()-1].w>val.w){
                t[x].push_back(Node(val.c,val.w));
            }
            else if(lasrs!=inf){
                if(lasrs<=c[x])t[x].push_back(Node(val.c+lasrs,val.w)),T[x][val.w]=make_pair(0,-lasrs);
                else t[x].push_back(Node(val.c+c[x],val.w)),T[x][val.w]=make_pair(lasrs-c[x],-c[x]);
            }
            else t[x].push_back(Node(val.c+c[x],val.w));
        }
        else{
            
            if(t[ls].size()!=0 && t[ls][t[ls].size()-1].w>val.w){
                t[x].push_back(Node(val.c,val.w));
            }
            else if(lasls!=inf){
                t[x].push_back(Node(val.c+lasls,val.w));T[x][val.w]=make_pair(0,-lasls);
            }
        }
        if(bel==0)lasls=min(lasls,val.c);
        else lasrs=min(lasrs,val.c);
    }
}
ll ide(ll val,ll d){
    return (le[val]&(1<<(n-d-1)))?1:0;
}
void solve2(ll x,ll opt){
    if(x>=(1<<n))return ans[++tot]=q[x],void();
    if(opt==-1){
        for(auto val:t[x]){
            if(val.c>k)continue;
            ll d=__lg(x);ll id=ide(val.w,d);k-=val.c;
            solve2((x<<1)+id,val.w);
            if(k>=T[x][val.w].first)k-=T[x][val.w].second;
            solve2((x<<1)+(id^1),-1);
            break;
        }
    }
    else{
        ll d=__lg(x),id=ide(opt,d);
        solve2((x<<1)+id,opt);if(k>=T[x][opt].first)k-=T[x][opt].second;
        solve2((x<<1)+(id^1),-1);
    }
}
void solve(){
    n=read(),k=read();
    for(ll i=1;i<(1<<n);i++)c[i]=read();
    for(ll i=(1<<n);i<(1<<(n+1));i++)q[i]=read(),le[q[i]]=i;
    dfs(1);solve2(1,-1);tot=0;
    for(ll i=1;i<=(1<<n);i++)write(ans[i]),putchar(' ');
    for(ll i=1;i<=(1<<(n+1));i++)t[i].clear(),mar[i]=0,T[i].clear();putchar('\n');
}
int main(){
    ll T=read();while(T--)solve();
    return 0;
}

                
            
        
浙公网安备 33010602011771号