B0628 模拟赛题解

spider-oyster / 2023-06-29 / 原文

原题链接

前言

隔天考试食不食油饼。

感受:

难度还是佛如 C 组。

T1 用 1.5 h 想出正解,是道比较好想的博弈论。

T2 一开始居然想的树剖(脑子抽了),实际可能比 T1 更简单?暴力甚至挂了。

T3 没看。

T4 打了 30分暴力,用 ST 表卡了下内存。

挂分情况:

T2 暴力奇怪的挂了 -30。

T1 博弈

关键点在于谁先破开一个环。

显然,先破开环的人被动。

有两个特殊性质:

  • 被破开的有 3 个绳结的绳子必定被一次取完,且取得人要再破开一个环。

  • 被破开的有 4 个绳结的绳子可以被劈成 2,2,此时下一人必定会取完并破开下一个环。

考虑到取完要破一个环,贪心的想,肯定先按绳结排序。

注意到被破开的大于等于 4 个绳结的绳子是个很关键的点,此轮的人要么留 4 个取走其他,要么取走全部并破开下一个环,令自己被动。

注意被破开的有 3 个绳结的绳子无法保留,只能取完闭并破环,应特殊处理。

现在假设只有大于等于 4 个绳结的绳子。

定义 \(f(i,t,x,y)\) 表示取到第 \(i\) 个绳子,当前是 先手(0)/后手(1) 拿,此时先手有 \(x\) 个,后手有 \(y\) 个,当前拿的人能拿到的最大值。

定义 \(s\)\(a\) 的前缀和数组。

那么此时要么取走 \(s_n-s_{i-1}-4\cdot (n-i)\),要么取完 \(a_i\),并让另一个人主动。

据此,不难写出 \(f\) 的递归式。

时间复杂度 \(O(n \log n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;

int n,c,x,y,t,ans;
int a[105],s[105];

int f(int i,int t,int x,int y)
{
    if(i==n) return (t?y:x)+a[n];
    return max((t?y:x)+s[n]-s[i-1]-4*(n-i),s[n]-f(i+1,t^1,x+a[i]*(!t),y+a[i]*t));
}

signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
    for(int i=1;i<=n;i++) if(a[i]==3) c++;
    if(c&1) x+=c/2*3,y+=c/2*3+3,t=0;
    else x+=c/2*3,y+=c/2*3,t=1;
    if(c<n) ans=f(c+1,t,x,y);
    else ans=x;
    if(t) ans=s[n]-ans;
    cout<<ans<<' '<<s[n]-ans<<'\n';
}

T2 齿轮

据 s.f.s.l.,齿轮转速只和两个齿轮有关(还说这是常识),被薄纱了。

的确,化简式子后发现只和给定的齿轮 \(x,y\) 的齿轮数相关。

可以用并查集维护。扩展域 \(x+n\) 为与 \(x\) 方向相反的齿轮。

这题就完了。

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,m,a[N],fa[N*2];
inline int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}

inline int rd()
{
    int x=0;char c=getchar();
    for(;!isdigit(c);c=getchar());
    for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48);
    return x;
}

int main()
{
    n=rd(),m=rd();
    for(int i=1;i<=n;i++) a[i]=rd();
    for(int i=1;i<=n*2;i++) fa[i]=i;
    while(m--)
    {
        int op=rd(),x=rd(),y=rd(),v;
        if(op==1) a[x]=y;
        else if(op==2)
        {
            if(x==y) continue;
            fa[find(x+n)]=find(y);
            fa[find(y+n)]=find(x);
        }
        else
        {
            v=rd();
            if(fa[find(x)]==fa[find(y)]&&fa[find(x+n)]==fa[find(y)]) {puts("0");continue;}
            if(fa[find(x)]!=fa[find(y)]&&fa[find(x+n)]!=fa[find(y)]) {puts("0");continue;}
            long long A=1ll*a[x]*v,B=a[y],g=__gcd(A,B);A/=g,B/=g;
            if(fa[find(x+n)]==find(y)) A=-A;
            printf("%lld/%lld\n",A,B);
        }
    }
}

T3

题意可简化为构造一个不下降序列 \(a,|a|=n\),使 \(a_n\) 最小,且满足 \(\prod\limits_{i=1}^{n}(a_i-i+1)=c\)\(c \leq 10^9\)

\(A_i=a_i-i+1\),则 \(A_i-1 \leq A_i\)。显然 \(A_i \mid c\)

根据因数个数公式,\(10^9\) 范围内的数因数个数大约在 \(1500\) 个以内。

\(1\) 是个很好用的构造数字,可以用来补位置。

于是贪心地想,构造时要让构造的序列尽可能短。

歪解(别急着走,万一是 100pts 还贼简单呢)

枚举 \(a_n\),每次往前贪心地选最大满足条件的因数,如此构造。

把所有因数丢进一个 set 即可。复杂度 \(O(n\log d)\)\(d\) 为因数个数。

#include<bits/stdc++.h>
using namespace std;

int T,n,c;
set<int> d;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>T;
    while(T--)
    {
        d.clear();
        cin>>n>>c;
        for(int i=1;i*i<=c;i++) if(c%i==0) d.insert(i),d.insert(c/i);
        for(int i:d)
        {
            vector<int> p;
            p.push_back(i);
            int _c=c/i;
            while(p.size()<n&&_c!=1)
            {
                auto it=d.upper_bound(*p.rbegin());
                while(it!=d.begin()&&(*it-1>*p.rbegin()||_c%*it!=0)) --it;
                if(*it==1) break;
                p.push_back(*it);_c/=*it;
            }
            if(_c!=1) continue;
            reverse(p.begin(),p.end());
            int cnt=n-p.size();
            // 补 0
            for(int i=1;i<=cnt;i++) cout<<i<<' ';
            for(int i=0;i<p.size();i++) cout<<p[i]+cnt+i<<' ';
            cout<<'\n';
            break;
        }
    }
}

正解

为什么是歪解?

来看一组反例。

比如要得到 \(12113374=2\times 7 \times 13 \times 19 \times 31 \times 113\),此时要求选的数 \(\leq 266\)

如果按歪解的方案:就是 \(266\times 113\times 31\times 13\),需要 \(4\) 个数,

但实际只需要 \(3\) 个数:\(247\times 226\times 217\)

但这种数据实在太难构造,所以没有。

事实上,贪心地想,答案序列可以表示为如此的形式 :\(...,y,y-1,y-2,...,x\)

其中前半段最大值 \(\leq y\)。因为对 \(A_i\) 的限制为 \(A_i-1 \leq A_i\),这样的形式能让 \(x\) 尽可能小。注意 \(y,y-1,...\) 这类值还可以在前面出现,显然也合法,只是上述形式更方便计算。

考虑枚举 \(x\),递增地枚举 \(y\),记 \(f(i,j)\) 为使用 \(\leq d_i\) 的因数构造出 \(d_j\) 值的最小长度。(\(d\)\(c\) 的因数数组)。

此时就能 \(O(1)\) 判断当前 \(x,y\) 是否合法。

转移方程还是写一下:

\(\begin{cases} f(i,j)=f(i-1,j)\qquad d_i \nmid d_j \\ f(i,j)=\min\{f(i-1,j),f(i,k)+1\}\qquad d_i \mid d_j \end{cases}\)

其中 \(k\) 为因数 \(d_j/d_i\) 的下标,这个可以用哈希表维护。

注意还要记录路径。

#include<bits/stdc++.h>
using namespace std;

const int N=1500;
int T,n,c,k;
int d[N],f[N][N];
pair<int,int> g[N][N];//路径数组
unordered_map<int,int> mp;//哈希表

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>T;
    while(T--)
    {
        k=0;
        cin>>n>>c;
        for(int i=1;i*i<=c;i++) if(c%i==0) d[++k]=i,d[++k]=c/i;
        if(d[k-1]==d[k]) --k;
        sort(d+1,d+1+k);
        for(int i=1;i<=k;i++) mp[d[i]]=i;
        for(int i=0;i<=k;i++) for(int j=0;j<=k;j++) f[i][j]=1e9;
        f[0][1]=0;
        for(int i=1;i<=k;i++)
            for(int j=1;j<=k;j++)
            {
                f[i][j]=f[i-1][j];
                g[i][j]={i-1,j};
                if(d[j]%d[i]==0)
                {
                    int k=mp[d[j]/d[i]];
                    if(f[i][k]+1<f[i][j]) f[i][j]=f[i][k]+1,g[i][j]={i,k};
                }
            }
        int ti,tj;
        vector<int> p;
        for(int i=1;i<=k;i++)
        {
            int x=d[i],_c=c/x,flag=0;
            //特判 y=x 的情况
            if(f[i][mp[_c]]+1<=n) {p.push_back(x),ti=i,tj=mp[_c];break;}
            for(int j=i+1;j<=k&&d[j]-d[j-1]<=1&&_c%d[j]==0;j++)
            {
                _c/=d[j];
                if(f[j][mp[_c]]+j-i+1<=n)
                {
                    for(int k=i;k<=j;k++) p.push_back(d[k]);
                    ti=j,tj=mp[_c],flag=1;
                    break;
                }
            }
            if(flag) break;
        }
        while(ti&&tj)
        {
            auto [_ti,_tj]=g[ti][tj];
            if(_ti==ti) p.push_back(d[ti]);
            ti=_ti,tj=_tj; 
        }
        reverse(p.begin(),p.end());
        int cnt=n-p.size();
        // 补 0
        for(int i=1;i<=cnt;i++) cout<<i<<' ';
        for(int i=0;i<p.size();i++) cout<<p[i]+cnt+i<<' ';
        cout<<'\n';
    }
}

T4 简单的数据结构题

放在 T4,雀食简单

如果刻意去研究完全平方数性质,那你就被骗到了。

考虑 \(x\) 与运算上一些数,值的变化不会超过 \(\log x\) 次(把所有位的 1 变为 0 后就不会变化了)。

如果固定左端点,把所有使值变化的位置处理出来,就能得到 \(\log\) 级别个区间,每个区间值是相同的。

判断当前值是否是完全平方数,就能统计答案。

对于询问,是典型的二维偏序问题,离线下来,排个序,统计答案用线段树维护即可。

处理区间带个 \(\log\),每次用线段树统计答案带个 \(\log\),总复杂度 \(O(n\log n \log v+q \log n)\),其中 \(v\) 为值域。

也就调了一个小时。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5;
int _,n,m,a[N],ans[N*5],nxt[N][30];
struct node{int l,r,id;} q[N*5];
struct SegmentTree{
    #define mid ((l+r)>>1)
    int sum[N<<2],add[N<<2];
    inline void clear() {for(int i=1;i<=n*4;i++) sum[i]=add[i]=0;}
    inline void pushdown(int k,int l,int r)
    {
        if(!add[k]) return;
        add[k<<1]+=add[k],add[k<<1|1]+=add[k];
        sum[k<<1]+=(mid-l+1)*add[k],sum[k<<1|1]+=(r-mid)*add[k];
        add[k]=0;
    }
    void upd(int x,int y,int v,int k=1,int l=1,int r=n)
    {
        if(l>=x&&r<=y) {sum[k]+=(r-l+1)*v,add[k]+=v;return;}
        pushdown(k,l,r);
        if(x<=mid) upd(x,y,v,k<<1,l,mid);
        if(mid<y) upd(x,y,v,k<<1|1,mid+1,r);
        sum[k]=sum[k<<1]+sum[k<<1|1];
    }
    int qsum(int x,int y,int k=1,int l=1,int r=n)
    {
        if(l>=x&&r<=y) return sum[k];
        int res=0;
        pushdown(k,l,r);
        if(x<=mid) res+=qsum(x,y,k<<1,l,mid);
        if(mid<y) res+=qsum(x,y,k<<1|1,mid+1,r);
        return res;
    }
}T;

inline int rd()
{
    int x=0;char c=getchar();
    for(;!isdigit(c);c=getchar());
    for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48);
    return x;
}

signed main()
{
    //判断是否为完全平方数
    auto check=[](int x) {return (int)sqrt(x)*sqrt(x)==x;};
    _=rd();
    while(_--)
    {
        n=rd(),m=rd();
        T.clear();
        for(int i=1;i<=n;i++) a[i]=rd();
        for(int i=1;i<=m;i++) q[i]={rd(),rd(),i};
        sort(q+1,q+1+m,[&](node a,node b){return a.l>b.l;});
        vector<int> t(31,-1);
        for(int i=n;i>=1;i--) for(int j=0;j<30;j++) nxt[i][j]=t[j],t[j]=((a[i]>>j)&1)?t[j]:i;
        for(int i=n,j=1;i;i--)
        {
            vector<pair<int,int>> pos;
            for(int j=0;j<30;j++) if(((a[i]>>j)&1)&&nxt[i][j]!=-1) pos.emplace_back(nxt[i][j],j);
            sort(pos.begin(),pos.end());
            int l=i,now=a[i];
            //值相同的区间是左闭右开(EarthMessenger 狂喜)
            for(int j=0;j<pos.size();j++)
            {
                int tmp=now;
                now^=(1<<pos[j].second);
                //当前可能有多个位进行了变化
                while(j+1<pos.size()&&pos[j].first==pos[j+1].first) now^=(1<<pos[++j].second);
                if(check(tmp)) T.upd(l,pos[j].first-1,1);
                l=pos[j].first;
            }
            if(check(now)) T.upd(l,n,1);
            while(j<=m&&q[j].l==i) ans[q[j].id]=T.qsum(i,q[j].r),++j;
            
        }
        for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
    }
}