2020年泉州市信息学国庆模拟赛(提高组) 题解

Oct.1st-Oct.6th,2020

Guide
D1T1 D1T2 D1T3 D1T4
D2T1 D2T2 D2T3 D2T4
D3T1 D3T2 D3T3 D3T4
D4T1 D4T2 D4T3 D4T4
D5T1 D5T2 D5T3 D5T4
D6T1 D6T2 D6T3 D6T4


D1T1:回家(home)

显然,解方程 $\frac{ans(ans+1)}{2}\geq X$ 即可。 可暴力枚举,时间复杂度 $O( \sqrt{X} )$ 。

#include<iostream>
#include<cstdio>
using namespace std;
int x,i;
int main()
{
    freopen("home.in","r",stdin);
    freopen("home.out","w",stdout);
    scanf("%d",&x);
    for(i=1;i*(i+1)/2<x;i++);//枚举
    printf("%d",i);
    return 0;
}

D1T2:不必要(unnecessary)

分析题意后可以发现,对于每张牌,计算去掉它之后其它的牌是否能够凑成 $[k-a_i,k)$ ,即可。可以用 01 背包逆运算来做,时间复杂度 $O(NK)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
const int N=5e3+100;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,k,ans;
int a[N],f[N],g[N];
int main()
{
    freopen("unnecessary.in","r",stdin);
    freopen("unnecessary.out","w",stdout);
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    f[0]=1;
    for(int i=1;i<=n;i++)
        for(int j=k;j>=a[i];j--)
            f[j]+=f[j-a[i]];//01 背包
    for(int i=1;i<=n;i++)
    {
        if(a[i]>=k)
            continue;
        memcpy(g,f,sizeof(f));
        bool pd=1;
        for(int j=1;j<k;j++)
        {
            if(j>=a[i])
                g[j]-=g[j-a[i]];//01 背包逆运算
            if(j+a[i]>=k && g[j])
                pd=0;//判断是否可行
        }
        ans+=pd;
    }
    printf("%d",ans);
    return 0;
}

D1T3:最大权值(sequence)

可以发现对于一段连续序列的 gcd 不超过 $\log {N}$ ,因此可以存储之前所有的 gcd ,每次更新即可。可以用链表维护,时间复杂度 $O(N\log N)$ 。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e5+100;
int n,tot,head,tail;
int h[N],t[N],l[N];
ll ans;
ll a[N],v[N];
ll gcd(ll x,ll y)
{
    return x?gcd(y%x,x):y;
}
int main()
{
    freopen("sequence.in","r",stdin);
    freopen("sequence.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    head=h[2]=1,tail=t[1]=2,tot=2,v[1]=-1;
    for(int i=1;i<=n;i++)
    {
        for(int j=t[head];j!=tail;j=t[j])//枚举之前的 gcd
        {
            ll now=gcd(v[j],a[i]);
            ans=max(ans,now*(i-l[j]+1));
            if(now!=v[j])//有变动
            {
                if(now==v[h[j]])
                    h[t[j]]=h[j],t[h[j]]=t[j];
                else
                    v[j]=now;
            }
        }
        if(a[i]!=v[h[tail]])//删除
            h[++tot]=h[tail],t[tot]=tail,t[h[tail]]=tot,h[tail]=tot,l[tot]=i,v[tot]=a[i],ans=max(ans,a[i]);
    }
    printf("%lld",ans);
    return 0;
}

D1T4:改造陷阱(trap)

把整个陷阱分成 $[1,m][1,m],[1,m][m+1,n],[m+1,n][1,n],[m+1,n][m+1,n]$ 四个部分,可以发现,知道行 $[m]$ 和列 $[m]$ 后,只要知道任意一个区域的值就可以求出其它四个区域的值。于是可以枚举这三个值进行计算。因为对于当前列的行 $[m]$ 值枚举其它列是无用的,因此可以一列一列计算,免去枚举其它列的复杂度。时间复杂度 $O(m^22^m)$ 。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int N=50;
int n,m,ans;
int a[N][N];
int main()
{
    freopen("trap.in","r",stdin);
    freopen("trap.out","w",stdout);
    scanf("%d",&n);m=(n+1)/2;
    for(int i=1;i<=n;i++)    
        for(int j=1;j<=n;j++)
            scanf("%d",&a[i][j]);
    for(int i=0;i<(1<<m);i++)//枚举行
    {
        int now=((i&1)?-1:1)*a[m][m];//[m][m]
        for(int x=1;x<m;x++)
            now+=((i>>x&1)?-1:1)*a[x][m]+((i&1^i>>x&1)?-1:1)*a[x+m][m];//计算列 [m] 的值
        for(int y=1;y<m;y++)
        {
            int nowy=0;
            for(int j=0;j<2;j++)//枚举当前列的行 [m] 值
            {
                int nowj=(j?-1:1)*a[m][y]+((j^i&1)?-1:1)*a[m][y+m];//计算
                for(int x=1;x<m;x++)
                    nowj+=abs(a[x][y]+((i>>x&1)?-1:1)*a[x][y+m]+(j?-1:1)*a[x+m][y]+((j^i&1^i>>x&1)?-1:1)*a[x+m][y+m]);//计算答案
                nowy=max(nowy,nowj);
            }
            now+=nowy;
        }
        ans=max(ans,now);
    }
    printf("%d",ans);
    return 0;
}

D2T1:剪纸(cut)

对于每个字母,找到它在每个字符串中的最小出现次数,输出即可。注意字典序最小。时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
const int N=100;
int n;
int cnt[N][30];
int main()
{
    freopen("cut.in","r",stdin);
    freopen("cut.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        for(int j=0;j<s.size();j++)
            cnt[i][s[j]-'a']++;
    }
    for(int i=0;i<26;i++)//从小到大枚举
    {
        int now=N;
        for(int j=1;j<=n;j++)
            now=min(now,cnt[j][i]);//找最小
        for(int j=0;j<now;j++)
            cout<<(char)(i+'a');
    }
    puts("");
    return 0;
}

D2T2:矩形(rect)

把题目转化为以线段长作为数值的矩阵的所有子矩阵之和,显然这个子矩阵的累加次数是有规律的。可以发现同列或同行从两边到中间的累加次数之差是一个公差为 $2$ 的等差数列。暴力分别计算再相乘即可,时间复杂度 $O(n+m)$ 。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e5+100;
const ll P=1e9+7;
int n,m;
ll sumx,sumy;
int x[N],y[N],dx[N],dy[N];
int main()
{
    freopen("rect.in","r",stdin);
    freopen("rect.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&x[i]);
    for(int i=1;i<=m;i++)
        scanf("%d",&y[i]);
    n--,m--;
    for(int i=1;i<=n;i++)
        dx[i]=x[i+1]-x[i];
    for(int i=1;i<=m;i++)
        dy[i]=y[i+1]-y[i];//线段长
    for(ll i=1,j=n,k=n;i*2<=n+1;i++,j-=2,k+=j)
        if(i*2<=n)
            sumx=(sumx+1LL*(dx[i]+dx[n-i+1])*k)%P;
        else
            sumx=(sumx+1LL*dx[i]*k)%P;
    for(ll i=1,j=m,k=m;i*2<=m+1;i++,j-=2,k+=j)
        if(i*2<=m)
            sumy=(sumy+1LL*(dy[i]+dy[m-i+1])*k)%P;
        else
            sumy=(sumy+1LL*dy[i]*k)%P;//行列分别计算
    printf("%lld",(sumx*sumy)%P);
    return 0;
}

D2T3:贸易(trade)

可以发现,当在任意一点购买商品后,接下来的方案都是相同的。因此可以先预处理出这个方案,即在一个点购买商品后下一个点是什么。可以发现这些点可以构成一棵树,在这棵树上走即可。对于每个询问,先找到第一个买东西的点。使用倍增优化,时间复杂度 $O(n\log n)$ 。

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+100,T=18;
int n,q,tot,tot1,cnt,Max;
int head[N],ver[N],Next[N];
int h1[N],v1[N],n1[N];
int a[N],d[N],P[20],f[N][20][2],f1[N][20];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
void add1(int x,int y)
{
    v1[++tot1]=y,n1[tot1]=h1[x],h1[x]=tot1;
}
void dfs(int x,int fa)
{
    d[x]=d[fa]+1;
    f[x][0][0]=a[fa],f[x][0][1]=fa;
    for(int i=1;i<=T;i++)
        f[x][i][0]=max(f[x][i-1][0],f[f[x][i-1][1]][i-1][0]),f[x][i][1]=f[f[x][i-1][1]][i-1][1];
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        dfs(y,x);
    }
}//原树倍增预处理
void dfs1(int x,int fa)
{
    f1[x][0]=fa;
    for(int i=1;i<=T;i++)
        f1[x][i]=f1[f1[x][i-1]][i-1];
    for(int i=h1[x];i;i=n1[i])
    {
        int y=v1[i];
        dfs1(y,x);
    }
}//新树倍增预处理
void pre()
{
    dfs(1,0);
    for(int i=1;i<=n;i++)
    {
        int x=i,p;
        for(int j=T;j>=0;j--)
            if(f[x][j][0]<=a[i] && f[x][j][1])
                x=f[x][j][1];
        p=f[x][0][1];
        add1(p,i);
    }//倍增找下一个买东西的点
    dfs1(0,0);
}
int solve(int u,int v,int c)
{
    int x=u,now=0;
    if(a[u]<=c)
    {
        for(int j=T;j>=0;j--)
            if(f[x][j][0]<=c && f[x][j][1])
                x=f[x][j][1];
        x=f[x][0][1];
    }//倍增找第一个买东西的点
    if(x && d[x]>=d[v])
        now++;
    else
        return 0;
    for(int j=T;j>=0;j--)
        if(d[f1[x][j]]>=d[v])
            x=f1[x][j],now+=P[j];//新树倍增计算
    return now;
}
int main()
{
    freopen("trade.in","r",stdin);
    freopen("trade.out","w",stdout);
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),Max=max(Max,a[i]);
    P[0]=1;
    for(int i=1;i<=T;i++)
        P[i]=P[i-1]*2;
    for(int i=1,x,y;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    pre();
    for(int i=1,x,y,z;i<=q;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        printf("%d\n",solve(x,y,z));
    }
    return 0;
}

D2T4:字符操作(transformation)

分析后可以发现操作是可逆的并且当 $A$ 或 $B$ 的数量超过 $3$ 后对 $3$ 取余后是等价的,并且字符顺序对答案无影响。因此可以将所有的情况变成数量小于 $3$ 的情况,对这写情况打表判断即可。时间复杂度 $O(|S|+|T|+q)$ 。

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
const int N=1e5+100;
int q;
int pd[3][3]={{3,2,1},{1,3,2},{2,1,3}}/*打表*/,sums[N][2],sumt[N][2];
string s,t;
int main()
{
    freopen("transformation.in","r",stdin);
    freopen("transformation.out","w",stdout);
    cin>>s>>t;scanf("%d",&q);
    for(int i=0;i<s.size();i++)
        sums[i+1][s[i]-'A']=sums[i][s[i]-'A']+1,sums[i+1][1-(s[i]-'A')]=sums[i][1-(s[i]-'A')];
    for(int i=0;i<t.size();i++)
        sumt[i+1][t[i]-'A']=sumt[i][t[i]-'A']+1,sumt[i+1][1-(t[i]-'A')]=sumt[i][1-(t[i]-'A')];
    for(int i=1,a,b,c,d;i<=q;i++)
    {
        scanf("%d%d%d%d",&a,&b,&c,&d);
        int sa=(sums[b][0]-sums[a-1][0])%3,sb=(sums[b][1]-sums[a-1][1])%3,ta=(sumt[d][0]-sumt[c-1][0])%3,tb=(sumt[d][1]-sumt[c-1][1])%3;//对 3 取模
        puts((pd[sa][sb]==pd[ta][tb])?"YES":"NO");//判断
    }
    return 0;
}

D3T1:生物信息学(bioinformatics)

发现 $k$ 很小,可以用一个 $10$ 位四进制数存储状态,暴力判断即可。时间复杂度 $O(n+4^k)$ 。

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
const int N=1e6+5e5;
int k,Max,ans;
int cnt[N];
string s;
int main()
{
    freopen("bioinformatics.in","r",stdin);
    freopen("bioinformatics.out","w",stdout);
    cin>>s;scanf("%d",&k);Max=1<<(k<<1);
    for(int i=0;i<s.size() && i+k<=s.size();i++)
    {
        int now=0;
        for(int j=0;j<k;j++)
        {
            if(s[i+j]=='G')
                now+=(1<<(j<<1));
            if(s[i+j]=='C')
                now+=(2<<(j<<1));
            if(s[i+j]=='T')
                now+=(3<<(j<<1));
        }
        cnt[now]++;//统计
    }
    for(int i=0;i<Max;i++)
        ans=max(ans,cnt[i]);
    printf("%d",ans);
    return 0;
}

D3T2:符号(sgn)

显然序列的任意两个相邻的数的符号都是相反的。枚举两种情况,贪心操作即可。时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
const int N=1e5+100;
int n;
int a[N];
ll sum,ans,anss=0x3f3f3f3f3f3f3f3f;
int main()
{
    freopen("sgn.in","r",stdin);
    freopen("sgn.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int k=0,p=-1;k<2;k++,p=1) //枚举
    {
        sum=ans=0;
        for(int i=1;i<=n;i++)
        {
            sum+=a[i];
            if(p==1)
            {
                p=-1;
                if(sum<0)
                    continue;
                ans+=sum+1;
                sum=-1;
            }
            else
            {
                p=1;
                if(sum>0)
                    continue;
                ans+=1-sum;
                sum=1;
            }//贪心
        }
        anss=min(anss,ans);
    }
    printf("%lld",anss);
    return 0;
}

D3T3:无穷序列(sequence)

分析题意后发现对于序列的前两位数可以分为 ${1,1}$,${1,>1}$,${>1,>1}$ 三种情况,分别讨论。发现可以 $DP$ 解,推出状态转移方程 $f_i=f_{i-1}+\sum_{j=1}^{i-3} f[j]+(n-1)^2+n-i+2$ ,$\sum_{j=1}^{i-3}$ 可以用前缀和计算,初值 $f_1=n,f_2=n^2$。时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=2e6+100;
const ll P=1e9+7;
int n;
ll sum;
ll f[N];
int main()
{
    freopen("sequence.in","r",stdin);
    freopen("sequence.out","w",stdout);
    scanf("%d",&n);f[1]=n,f[2]=1LL*n*n%P;
    for(int i=3;i<=n;i++)
    {
        sum=(sum+f[i-3])%P;
        f[i]=(f[i-1]+sum+1LL*(n-1)*(n-1)+n-i+2)%P;
    }
    printf("%lld",f[n]);
    return 0;
}

D3T4:集合操作(set)

按照题目要求判断,可以用一个队列维护被删除的数,那么答案就是没有被插入过的最小的数和被删除的最小的数中更小的。另外可以发现,如果一个数比另一个数早删除,还比这个数大,那么它就是无用的,可以删除。时间复杂度 $O(Tm)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=2e6+100,P=998244353;
int T,m,seed,a,b,c,d,cnt2,cnt3,head,tail,p,ans;
int del[N],q[N];
bool pd[N],v[N]; 
unsigned see;
unsigned gen_rand_int() 
{
    see^=see<<13;
    see^=see>>17;
    see^=see<<5;
    return see;
}
void clear()
{
    memset(del,0,sizeof(del));
    memset(q,0,sizeof(q));
    memset(pd,0,sizeof(pd));
    memset(v,0,sizeof(v));
    cnt2=cnt3=ans=tail=0,head=1,p=a+1;
}
int main()
{
    freopen("set.in","r",stdin);
    freopen("set.out","w",stdout);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d%d%d%d%d",&m,&see,&a,&b,&c,&d);
        clear();
        for(int i=0;i<=a;i++)
            pd[i]=v[i]=1;
        for(int i=1,x;i<=m;i++)
        {
            if (gen_rand_int()%c==0)
                x=-1;
            else
                x=gen_rand_int()%b;
            if(x==-1)//事件3
            {
                if(d || cnt2<=cnt3)
                    continue;
                if(head<=tail && q[head]==del[++cnt3])
                    head++;
                pd[del[cnt3]]=1;
            }
            else
                if(!v[x])//事件1
                {
                    v[x]=pd[x]=1;
                    while(v[p])
                        p++;
                }
                else
                    if(pd[x])//事件2
                    {
                        if(d)
                            continue;
                        while(head<=tail && q[tail]>x)//删除
                            tail--;
                        q[++tail]=del[++cnt2]=x;
                        pd[x]=0;
                    }
                    else
                        if(cnt3<cnt2)//事件3
                        {
                            if(d)
                                continue;
                            if(head<=tail && q[head]==del[++cnt3])
                                head++;
                            pd[del[cnt3]]=1;
                        }
                        else
                            continue;
            int now=p;
            if(head<=tail)
                now=min(now,q[head]);
            ans^=1LL*i*(i + 7)%P*now%P;
        }
        printf("%d\n",ans);
    }
    return 0;
}

D4T1:澡堂(shower)

按题意模拟即可,时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
using namespace std;
const int N=2e5+100;
int n,t,ans;
int a[N];
int main()
{
    freopen("shower.in","r",stdin);
    freopen("shower.out","w",stdout);
    scanf("%d%d",&n,&t);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        if(i>1)
            ans+=min(t,a[i]-a[i-1]);//加上
    }
    ans+=t;//最后一次
    printf("%d",ans);
    return 0;
}

D4T2:背包(knapsack)

01背包,可以发现数量很小,但体积极大。发现只有四种体积的物品并且分别为 $[w_1,w_1+3]$ ,考虑枚举放入的物品数量,减掉 $w_1$ 后可以使体积变小很多,若可以全部放入则直接特判不用计算。暴力 01 背包,时间复杂度 $O(n^4)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int N=150,M=500;
struct Node
{
    int w,v;
    #define w(i) a[i].w
    #define v(i) a[i].v
}a[N];
int n,W,Max,w1;
ll sum,ans,asum[N],f[N][M];
bool cmp(Node x,Node y)
{
    return x.v>y.v;
}
int main()
{
    freopen("knapsack.in","r",stdin);
    freopen("knapsack.out","w",stdout);
    scanf("%d%d",&n,&W);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&w(i),&v(i)),sum+=w(i),Max=max(Max,w(i));w1=w(1);
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++)
        asum[i]=asum[i-1]+v(i);
    for(int i=1;i<=n;i++)//枚举几个物品
    {
        if(1LL*(w1-1)*i>=W)
            break;
        int now=W-(w1-1)*i;
        ll nans=0;
        if(now>=(Max-w1+1)*i)//全部放入
            nans=asum[i];
        else
        {
            memset(f,0,sizeof(f));
            for(int j=1;j<=n;j++)
                for(int c=i;c;c--)
                    for(int k=now;k>=w(j)-w1+1;k--)
                        f[c][k]=max(f[c][k],f[c-1][k-w(j)+w1-1]+v(j));//01 背包
            for(int j=0;j<=now;j++)
                nans=max(nans,f[i][j]);
        }
        ans=max(ans,nans);
    }
    printf("%lld",ans);
    return 0;
}

D4T3:图染色(coloring)

若图中有一个以上的奇环,显然无解;若只有一个奇环,则在这个奇环上并且不在偶环上的边可行,这个可以用树上差分计算;若没有奇环,则不在环上的边可行,这个可以用 tarjan 计算。时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
#define ano ((i-1)^1)+1
using namespace std;
const int N=2e5+100;
int n,m,tot,id,ans,cnt,now;
int head[N],ver[2*N],Next[2*N];
int c[N],dfn[N],low[N],d[N],sum[N];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
bool dfs1(int x)
{
    bool pd=0;
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(!c[y])
        {
            c[y]=3-c[x];
            pd|=dfs1(y);
        }
        else
            pd|=(c[x]==c[y]);
    }
    return pd;
}//尝试染色找奇环
void dfs2(int x,int fa)
{
    dfn[x]=low[x]=++cnt;
    for(int i=head[x];i;i=Next[i])
    {
        if(i==fa)
            continue;
        int y=ver[i];
        if(!dfn[y])
            dfs2(y,ano);
        low[x]=min(low[x],low[y]);
        if(low[y]>dfn[x])
            ans++;
    }
}//tarjan
void dfs3(int x,int fa)
{
    d[x]=d[ver[fa]]+1;
    for(int i=head[x];i;i=Next[i])
    {
        if(i==fa)
            continue;
        int y=ver[i];
        if(!d[y])
        {
            dfs3(y,ano);
            sum[x]+=sum[y];
        }
        else
            if(d[y]<d[x])
                if((d[x]-d[y])&1)
                    sum[x]--,sum[y]++;
                else
                    sum[x]++,sum[y]--,now++;
    }
}//树上差分
int main()
{
    freopen("coloring.in","r",stdin);
    freopen("coloring.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1,x,y;i<=m;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
        if(!c[i])
        {
            c[i]=1;
            if(dfs1(i))
                if(!id)
                    id=i;
                else//多个奇环
                {
                    puts("0");
                    return 0;
                }
        }
    if(!id)//没有奇环
    {
        for(int i=1;i<=n;i++)
            if(!dfn[i])
                dfs2(i,0);
        printf("%d",ans);
    }
    else//一个奇环
    {
        dfs3(id,0);
        for(int i=1;i<=n;i++)
            ans+=(sum[i]==now);
        printf("%d",ans+(now==1));
    }
    return 0;
}

D4T4:开车(car)

处理出每个操作前的位置及操作后的有解范围。经过分析可以发现,若当前操作大于等于下一操作的有解范围的两倍,则有解范围相同,否则有解范围向外扩大当前操作的路程。然后判断每个操作前的位置是否在操作后的有解范围内即可。时间复杂度 $O(N+Q)$ 。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
const int N=5e5+100;
int n,D,Q;
int d[N],q[N],c[N],f[N];
int main()
{
    freopen("car.in","r",stdin);
    freopen("car.out","w",stdout);
    scanf("%d%d",&n,&D);
    for(int i=1;i<=n;i++)
        scanf("%d",&d[i]);
    scanf("%d",&Q);
    for(int i=1;i<=Q;i++)
        scanf("%d",&q[i]);
    c[0]=D;
    for(int i=1;i<=n;i++)
        c[i]=min(c[i-1],abs(c[i-1]-d[i]));//操作后的位置
    f[n+1]=1;
    for(int i=n;i;i--)
    {
        if(d[i]>=f[i+1]*2)
            f[i]=f[i+1];
        else
            f[i]=f[i+1]+d[i];
    }//有解范围
    for(int i=1;i<=Q;i++)
        puts((c[q[i]-1]>=f[q[i]+1])?"YES":"NO");
    return 0;
}

D5T1:Scc游戏(puzzle)

首先配对,若 S 够多,则答案为可凑成的数量;否则还能将两个 C 凑成一个 S ,即 4 个 C 凑一组。直接计算即可,时间复杂度 $O(1)$ 。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
ll n,m;
int main()
{
    freopen("puzzle.in","r",stdin);
    freopen("puzzle.out","w",stdout);
    scanf("%lld%lld",&n,&m);
    if(n>=m/2)//S 够多
        printf("%lld",m/2);
    else//S 不够
        printf("%lld",n+(m-n*2)/4);
    return 0;
}

D5T2:炸 药(dynamite)

很容易想到二分答案。判断可以用树形 DP ,存储以当前节点为根的子树中最远的未爆炸的和最近的被点燃的,分别设为 $f_{x,0},f_{x,1}$ 。若当前二分到的答案为 $t$ ,当前节点为 $x$ ,子节点为 $y$ ,则有:

$$f_{x,0}=max(f_{y,0}+1)$$
$$f_{x,1}=min(f_{y,1}+1)$$
$$f_{x,0}=max(f_{x,0},0)(a[x]=1)$$
$$f_{x,0}=-INF(f_{x,0}+f_{x,1}\leq t)$$
$$f_{x,1}=0,f_{x,0}=-INF,ans=ans+1(f_{x,0}=t)$$

若根节点周围还有未引爆的,则根节点也要点燃。时间复杂度 $O(n\log n)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=3e5+100,T=20,INF=0x3f3f3f3f;
int n,m,tot,ans=-1,now;
int head[N],ver[2*N],Next[2*N];
int a[N],f[N][2];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void dp(int x,int fa,int t)
{
    for(int i=head[x];i;i=Next[i])
    {
        int y=ver[i];
        if(y==fa)
            continue;
        dp(y,x,t);
        f[x][0]=max(f[x][0],f[y][0]+1);
        f[x][1]=min(f[x][1],f[y][1]+1);
    }
    if(a[x])
        f[x][0]=max(f[x][0],0);
    if(f[x][0]+f[x][1]<=t)
        f[x][0]=-INF;
    if(f[x][0]==t)
        f[x][1]=0,f[x][0]=-INF,now++;
}//树形 DP
bool check(int t)
{
    now=0;
    for(int i=1;i<=n;i++)
        f[i][0]=-INF,f[i][1]=INF;
    dp(1,0,t);
    if(f[1][0]>=0)
        now++;//根节点点燃
    return now<=m;
}
void solve()
{
    int l=0,r=n;
    while(l<=r)//二分答案
    {
        int mid=l+r>>1;
        if(check(mid))
        {
            r=mid-1;
            ans=mid;
        }
        else
            l=mid+1;
    }
}
int main()
{
    freopen("dynamite.in","r",stdin);
    freopen("dynamite.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1,x,y;i<n;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    solve();
    printf("%d",ans);
    return 0;
}

D5T3:涂色(color)

显然最大值和最小值一定是四个最值中的两个。若是同一颜色的,则把大的和小的分别涂两种颜色即可;否则就在刚刚的基础上,把一种颜色从小到大排序,然后不断翻转颜色,计算答案比较即可。时间复杂度 $O(n\log n)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define x(i) a[i].first
#define y(i) a[i].second
using namespace std;
const int N=2e5+100,INF=0x3f3f3f3f;
const ll inf=0x3f3f3f3f3f3f3f3f;
int n,xmax=-1,xmin=INF,ymax=-1,ymin=INF,Max=-1,Min=INF;
ll ans=inf,anss;
int main()
{
    freopen("color.in","r",stdin);
    freopen("color.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&x(i),&y(i));
        if(x(i)>y(i))
            swap(x(i),y(i));
        xmax=max(xmax,x(i));
        ymax=max(ymax,y(i));
        xmin=min(xmin,x(i));
        ymin=min(ymin,y(i));
    }
    anss=1LL*(xmax-xmin)*(ymax-ymin);//同一颜色
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
    {
        Max=max(Max,y(i));
        Min=min(Min,y(i));
        ans=min(ans,(ll)max(Max,x(n))-min(Min,x(i+1)));
    }//一个个翻转计算
    ans*=ymax-xmin;//不同颜色
    printf("%lld",min(ans,anss));
    return 0;
}

D5T4:割韭菜(chives)

可以发现每个时刻会割掉一定高度之上的所有韭菜,可以用类似计算斜率的方式计算。注意到不是每个韭菜都高到可以被割,因此要进行额外计算。可以利用单调栈维护割掉的高度,计算不正常收割的值。加上前缀和优化,时间复杂度 $O(n)$ 。

#include<iostream>
#include<cstdio>
#include<stack>
#define ll long long
using namespace std;
const int N=5e5+100,M=1e6+100;
int n,m,Max;
int a[M];
ll d[N],b[N],sum[M],v[N];
stack<int> s;
int main()
{
    freopen("chives.in","r",stdin);
    freopen("chives.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1,x;i<=n;i++)
    {
        scanf("%d",&x);Max=max(Max,x);
        a[x]++;
    }
    for(int i=1;i<=m;i++)
        scanf("%lld%lld",&d[i],&b[i]);
    for(int i=1;i<=Max;i++)
    {
        sum[i]=sum[i-1]+1LL*a[i]*i;
        a[i]+=a[i-1];//前缀和
    }
    s.push(0),s.push(0);
    for(int i=1;i<=m;i++)
    {
        int lv=Max,nv=max(v[s.top()],min((b[i]-b[s.top()])/(d[i]-d[s.top()]),1LL*Max));
        ll now=0;
        while(nv<lv)
        {
            now+=(d[i]-d[s.top()])*(sum[lv]-sum[nv])+(b[s.top()]-b[i])*(a[lv]-a[nv]);
            lv=nv;
            if(s.size()<=1 || lv>v[s.top()])
                break;
            s.pop();
            nv=max(v[s.top()],min((b[i]-b[s.top()])/(d[i]-d[s.top()]),1LL*Max));
        }
        v[i]=lv;
        if(v[i]<Max)
            s.push(i);
        printf("%lld\n",now);
    }//单调栈
    return 0;
}

D6T1:数码(digits)

发现每个数字都有一个,显然只要将所有数字对 $B-1$ 取余的值求出来,少用一个这个数字,剩下的数字从大到小排即可。注意若余数为 $0$ 则不用减。对于每个询问,二分查找即可。时间复杂度 $O(B+q\log B)$ 。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e6+100;
int b,q,sum;
ll summ;
int a[N],cnt[N];
ll Sum[N];
int main()
{
    freopen("digits.in","r",stdin);
    freopen("digits.out","w",stdout);
    scanf("%d%d",&b,&q);
    for(int i=0;i<b;i++)
        scanf("%d",&a[i]),sum=(sum+1LL*a[i]*i%(b-1))%(b-1),cnt[i]+=a[i],summ+=a[i];
    if(sum)
        cnt[sum]--,summ--;//去掉一个
    Sum[0]=cnt[0];
    for(int i=0;i<=b-1;i++)
        Sum[i]=Sum[i-1]+cnt[i];//计算数字用的位数
    for(int i=1;i<=q;i++)
    {
        ll k;
        scanf("%lld",&k);
        if(k>=summ)
        {
            puts("-1");
            continue;
        }
        int l=0,r=b-1,mid,ans;
        while(l<=r)//二分查找
        {
            mid=l+r>>1;
            if(Sum[mid]>k)
            {
                r=mid-1;
                ans=mid;
            }
            else
                l=mid+1;
        }
        printf("%d\n",ans);
    }
    return 0;
}

D6T2:路径规划(path)

可以发现这个路径一定是从起点到终点的一条路径然后在路径上或周边的某条最短的边反复行走得到的。设这条路径经过 $x$ 条边,则这条路就是起点到终点经过 $x$ 条边的最短路,并且 $x,N$ 奇偶性相同。因此我们枚举经过几条边,用 Bellman-ford 算法暴力求出最短路,在求最短路的同时求出路径经过的点周边的最短边,然后计算比较即可。时间复杂度 $O(T^3)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=1100,M=200;
const ll INF=0x3f3f3f3f;
int n,t,s,e,tot,Min;
ll ans=INF;
int head[N],ver[2*M],edge[2*M],Next[2*M];
int minv[N];
ll f[N][M][2];
bool v[N];
void add(int x,int y,int z)
{
    ver[++tot]=y,edge[tot]=z,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,edge[tot]=z,Next[tot]=head[y],head[y]=tot;
}
void query()
{
    bool pd=1;
    while(pd)
    {
        pd=0;
        for(int i=1;i<=tot;i++)
        {
            int x=ver[i],y=ver[ano],z=edge[i];
            if(y==e)//到终点
            {
                for(int j=1;j<=t;j++)
                    if(f[x][j-1][0]+z+min(f[x][j-1][1],(ll)minv[y])*(n-j)<f[y][j][0]+f[y][j][1]*(n-j))
                        f[y][j][0]=f[x][j-1][0]+z,f[y][j][1]=min(f[x][j-1][1],(ll)minv[y]),pd=1;
            }
            else
                for(int j=1;j<=t;j++)
                    if(f[x][j-1][0]+z<f[y][j][0])
                        f[y][j][0]=f[x][j-1][0]+z,f[y][j][1]=min(f[x][j-1][1],(ll)minv[y]),pd=1;
        }
    }
}
int main()
{
    freopen("path.in","r",stdin);
    freopen("path.out","w",stdout);
    scanf("%d%d%d%d",&n,&t,&s,&e);
    for(int i=1,x,y,z;i<=t;i++)
    {
        scanf("%d%d%d",&z,&x,&y);
        add(x,y,z);
    }
    for(int i=1;i<=1000;i++)
        for(int j=0;j<=t;j++)
            f[i][j][0]=f[i][j][1]=INF;
    f[s][0][0]=0;
    memset(minv,0x3f,sizeof(minv));
    for(int i=1;i<=tot;i++)
        minv[ver[i]]=min(minv[ver[i]],edge[i]);//周边最短边
    f[s][0][1]=minv[s];query();
    for(int i=min(t-((t^n)&1),n);i>=0;i-=2)
        ans=min(ans,f[e][i][0]+(n-i)*f[e][i][1]);//计算
    printf("%lld",ans);
    return 0;
}

D6T3:二叉搜索树(bst)

分析后可以发现对于一个子树的区间作为左儿子或右儿子的根节点是确定的。区间 DP 即可,可以先预处理出节点之间的 gcd 。时间复杂度 $O(Tn^3)$ 。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=300;
int T,n;
int a[N],Gcd[N][N];
bool pd;
bool f[N][N][2];
int gcd(int x,int y)
{
    return y?gcd(y,x%y):x;
}
int main()
{
    freopen("bst.in","r",stdin);
    freopen("bst.out","w",stdout);
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&n);
        memset(f,0,sizeof(f));
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]),f[i][i][0]=f[i][i][1]=1;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                Gcd[i][j]=gcd(a[i],a[j]);//预处理
        pd=0;
        for(int i=1;i<=n;i++)
        {
            for(int l=1;l+i-1<=n;l++)
            {
                int r=l+i-1;
                for(int p=l;p<=r;p++)//枚举根节点
                    if(f[l][p][0] && f[p][r][1])
                    {
                        if(l==1 && r==n)
                        {
                            pd=1;
                            break;
                        }
                        if(Gcd[l-1][p]!=1)
                            f[l-1][r][1]=1;
                        if(Gcd[p][r+1]!=1)
                            f[l][r+1][0]=1;
                    }
                if(pd)
                    break;
            }
            if(pd)
                break;
        }
        puts(pd?"Yes":"No");
    }
    return 0;
}

D6T4:跳跳跳(jump)

很容易想到暴力 DP ,可以用单调队列优化,保证答案不降且柱子高度递增。时间复杂度 $O(QN)$ 。

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6+100,M=50;
int n,Q;
int d[N],f[N],q[N];
int main()
{
    freopen("jump.in","r",stdin);
    freopen("jump.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&d[i]);
    scanf("%d",&Q);
    for(int i=1,k;i<=Q;i++)
    {
        scanf("%d",&k);
        int head=1,tail=q[1]=1;
        for(int j=2;j<=n;j++)
        {
            while(head<=tail && j-q[head]>k)//可行性维护
                head++;
            f[j]=f[q[head]]+(d[j]>=d[q[head]]);
            while(head<=tail &&(f[q[tail]]>f[j] ||(f[q[tail]]==f[j] && d[j]>=d[q[tail]])))//最优性维护
                tail--;
            q[++tail]=j;
        }//单调队列
        printf("%d\n",f[n]);
    }
    return 0;
}

Oct.8th,2020,于厦门外国语学校石狮分校

posted on 2020-10-08 21:24  TEoS  阅读(106)  评论(0编辑  收藏  举报