跳转至

20231005 B 组模拟赛 题解

前言

终于找到时间补以前的题解了。

这场我记得好像爆大零了,因为由于标题原因,我把这场当成 CSP 模拟赛就在冲 T1。事实证明不管什么考试还是应该先看一遍所有题再考虑做哪道。

T4 要用 【10】最小树形图,准备学了最小树形图再写。

密码是大小写敏感通用密码

T1

首先容易发现,若 \(a_1+b_1+c_1=a_2+b_2+c_2\),除非 \(a_1=a_2,b_1=b_2,c_1=c_2\),否则 \((a_1,b_1,c_1),(a_2,b_2,c_2)\) 必然不互相偏序,且容易发现若把总和相同的所有三元组都取完了,那么再加任意一个三元组必定会被偏序。

总和相同的三元组可以 \(O(n^2)\) 枚举,这道题可以 \(O(n^3)\) 过,\(O(n)\) 枚举总和就可过了,但其实你打表可以发现在 \(sum=\frac{3n}{2}\) 时个数取到最大值。

严谨证明可以参考附件里的官方题解。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=605,inf=0x3f3f3f3f;
int n,m;
struct Node{
    int x,y,z;
};
vector<Node> ans;
signed main(){
    n=read();m=(n*3)/2;
    forup(i,0,n){
        forup(j,0,n){
            int k=m-i-j;
            if(k<0||k>n) continue;
            ans.push_back(Node{i,j,k});
        }
    }
    printf("%d\n",(int)ans.size());
    for(auto i:ans){
        printf("%d %d %d\n",i.x,i.y,i.z);
    }
}

T2

首先直接做是不好做的,长度为实数的区间个数是正无穷的,考虑寻找性质。

首先容易发现若 \([l,r]\) 两端都不是某单一材质区间的(下文“区间”均指这个,询问区间用“\([l,r]\)”指代)端点,那么可以考虑左右两边哪边密度大调整到对应的端点(如果密度一样那么调整之后不会变劣)。故我们可以得出结论,\([l,r]\) 中必定有至少一边是区间端点。

但是现在区间个数显然还是正无穷的,考虑另一边。容易想到,若另一边不是区间端点,\([l,r]\) 的密度又不是上界 \(R\) 或下界 \(L\),那么这样必定是不优的,因为若最后一段密度较大,显然取到上界/下一个端点会比较优,若密度较小,显然取到下界/上一个端点会比较优。

那么我们可以得出结论了:最终的 \([l,r]\) 要么两边都是区间端点,要么一边是区间端点,区间质量取到 \(L\) 或 \(R\)。第二种情况区间个数是 \(O(n)\) 的,可以直接暴力全求出来,我们考虑第一种情况怎么求。

我们设质量的前缀和为 \(summ_i\),长度的前缀和为 \(suml_i\)。如果答案取到的区间为 \((s,t]\),密度为 \(P\),那么容易发现 \(\frac{summ_t-summ_s}{suml_t-suml_s}=P\),这玩意长得就很 01 分数规划,稍微移一下项容易发现 \(summ_r-Psuml_r=summ_l-Psuml_l\),然后就可以二分答案了。关于 checker,其实就是区间在线求最大值,可以单调队列维护。

复杂度 \(O(n\log n)\)。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=3e5+5,inf=0x3f3f3f3f;
const double eps=1e-7;
i64 n;
double l[N],sl[N],p[N],sm[N],L,R;
double ans=0;
void work(double M){
    i64 r=1;
    while(r<n&&sm[r+1]<=M) ++r;
    forup(i,1,n){
        while(r<n&&sm[r+1]-sm[i-1]<=M) ++r;
        double mm=sm[r]-sm[i-1],am=M-mm;
        double len=sl[r]-sl[i-1];
        len+=am/p[r+1];
        ans=max(ans,M/len);
    }
}
void dwork(double M){
    i64 r=n+1;
    while(r>1&&sm[n]-sm[r-2]<=M) --r;
    fordown(i,n,1){
        while(r>1&&sm[i]-sm[r-2]<=M) --r;
        double mm=sm[i]-sm[r-1],am=M-mm;
        double len=sl[i]-sl[r-1];
        len+=am/p[r-1];
        ans=max(ans,M/len);
    }
}
double a[N];
bool chk(double mm){
    forup(i,1,n){
        a[i]=sm[i]-sl[i]*mm;
    }
    deque<int> q;
    int r=1;
    forup(i,1,n){
        while(r<i&&sm[i]-sm[r]>=L){
            while(q.size()&&a[q.back()]>=a[r]) q.pop_back();
            q.push_back(r);
            ++r;
        }
        while(q.size()&&sm[i]-sm[q.front()]>R) q.pop_front();
        if(q.size()&&a[i]>=a[q.front()]) return true;
    }
    return false;
}
signed main(){
    n=read();L=read();R=read();
    forup(i,1,n){
        l[i]=read();
        sl[i]=sl[i-1]+l[i];
    }
    forup(i,1,n){
        p[i]=read();
        sm[i]=sm[i-1]+p[i]*l[i];
    }
    work(L);work(R);
    dwork(L);dwork(R);
    double ll=0,rr=1e6,mm;
    while(rr-ll>=eps){
        mm=(ll+rr)/2;
        if(chk(mm)) ll=mm;
        else rr=mm;
    }
    ans=max(ans,ll);
    printf("%.10lf",ans);
}

T3

树上问题通常可以优先考虑树形 DP,但是这道题给的信息不太好直接维护,因为切下来的块可能是“除去某棵子树”的部分。考虑转化一下。

其实也非常简单,我们看成两种操作,一种是保留当前子树,另一种是删除当前子树。而每个保留子树操作所删除的子树大小其实是很好求出的,这样所有信息都能在子树内维护了。

然后考虑怎么设计状态,由于 \(k\) 是很小的,考虑状态压缩。那么设 \(dp_{i,msk,j}\) 表示在 \(i\) 子树内,进行过的操作集合为 \(msk\),第一次保留子树的操作是 \(j\) 的方案数。为什么要记保留子树的操作呢?因为很多操作的可行性和这棵子树是否保留有关。若该子树内未进行保留子树操作 \(j\) 取 \(k+1\)。

然后考虑 DP 转移,假设现在要把 \(u,v\) 两结点的状态 \(dp_{u,s_1,j_1},dp_{v,s_2,j_2}\) 合并,那么两状态能合并,当且仅当:

  1. \((u,v)\in E\):不解释。
  2. \(j_1=k+1\vee j_2=k+1\):这个显然吧,不可能两棵子树同时都有保留子树操作,因为保留了一边的话另一边必然被删掉了。
  3. \(s_1\cap s_2=\varnothing\):这个也显然吧,一个操作只会进行一次。

那么我们可以枚举子集维护条件 \(3\),然后直接维护条件 \(2\),这一部分复杂度是 \(O(nk3^k)\) 的。

然后考虑对于状态 \(dp_{u,msk,j}\) 当前子树和父亲的边能不能断开作为一个全新的操作 \(j'\)。

首先,无论如何,\(j'\) 都不能在 \(msk\) 中,因为你不能进行一个操作两次,若这是一个保留子树操作,那么显然有 \(j'<j\),因为你不可能先保留后面的再保留前面的吧。若这是一个删除子树操作,那么必然 \(j'\) 比 \(msk\) 中最大值还大,因为假如你把这条边断开了,那么下面的子树就不存在了,显然是不对的。然后对于子树大小问题,可以简单维护,这里不做赘述。

复杂度 \(O(nk3^k+nk2^k)\)。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=5005,mod=998244353;
int n,m,a[N],s[N],sz[N],mx[1<<6|5];
vector<int> e[N];
int dp[N][1<<6|5][10],f[1<<6|5][10];
void dfs(int x,int fa){
    dp[x][0][m+1]=1;
    sz[x]=1;
    for(auto i:e[x]){
        if(i==fa) continue;
        dfs(i,x);
        sz[x]+=sz[i];
        forup(i,0,(1<<m)-1){
            forup(j,1,m+1){
                f[i][j]=dp[x][i][j];
                dp[x][i][j]=0;
            }
        }
        forup(s1,0,(1<<m)-1){
            int pp=((1<<m)-1)^s1;
            for(int s2=pp;s2;s2=(s2-1)&pp){
                int msk=s1|s2;
                forup(j,1,m){
                    if(mx[s2]<j) (dp[x][msk][j]+=1ll*f[s1][j]*dp[i][s2][m+1]%mod)%=mod;
                    if(mx[s1]<j) (dp[x][msk][j]+=1ll*f[s1][m+1]*dp[i][s2][j]%mod)%=mod;
                }
                (dp[x][msk][m+1]+=1ll*f[s1][m+1]*dp[i][s2][m+1]%mod)%=mod;
            }
            int s2=0,msk=s1|s2;
            forup(j,1,m){
                if(mx[s2]<j) (dp[x][msk][j]+=1ll*f[s1][j]*dp[i][s2][m+1]%mod)%=mod;
            }
            (dp[x][msk][m+1]+=1ll*f[s1][m+1]*dp[i][s2][m+1]%mod)%=mod;
        }
    }
    if(x==1) return;
    forup(i,0,(1<<m)-1){
        forup(j,1,m+1){
            f[i][j]=dp[x][i][j];
        }
    }
    forup(msk,0,(1<<m)-1){
        int sum=0;
        forup(j,1,m){
            if(msk&(1<<(j-1))){
                sum+=s[j];
            }
        }
        forup(k,mx[msk]+1,m){
            if(sum+s[k]==sz[x]){
                (dp[x][msk|(1<<(k-1))][m+1]+=f[msk][m+1])%=mod;
            }
        }
        forup(j,1,m+1){
            if(!f[msk][j]) continue;
            int nw=0;
            forup(k,1,j-1){
                if(msk&(1<<(k-1))){
                    nw+=s[k];
                    continue;
                }
                if(sz[x]==a[k]+nw){
                    (dp[x][msk|(1<<(k-1))][k]+=f[msk][j])%=mod;
                }
            }
        }
    }
}
signed main(){
    n=read();
    forup(i,1,n-1){
        int u=read(),v=read();
        e[u].push_back(v);
        e[v].push_back(u);
    }
    m=read();
    int nw=0;
    forup(i,1,1<<m){
        if(i==(i&-i)) ++nw;
        mx[i]=nw;
    }
    a[0]=n;
    forup(i,1,m){
        a[i]=read();
        s[i]=a[i-1]-a[i];
    }
    dfs(1,0);
    int ans=0;
    forup(i,1,m+1) (ans+=dp[1][(1<<m)-1][i])%=mod;
    printf("%d\n",ans);
}

评论