北海虽赊,扶摇可接;东隅已逝,桑榆非晚。|

QcpyWcpyQ

园龄:3年6个月粉丝:4关注:6

AtCoder Grand Contest

AGC001


A - BBQ Easy

简单贪心,两个两个一起选。

点击查看代码
const int N=1e6+5;
int n,a[N];

signed main(){
    n=read();n<<=1;
    for(int i=1;i<=n;i++)
        a[i]=read();
    sort(a+1,a+1+n);
    int ans=0;
    for(int i=1;i<=n;i+=2)
        ans+=a[i];
    write(ans);
    return 0;
}

B - Mysterious Light

发现与辗转相除类似,根据几何意义画图得出答案。

点击查看代码
int n,x;

inline int gcd(int x,int y){
    return y?gcd(y,x%y):x;
}

signed main(){
    n=read();x=read();
    write(3*(n-gcd(n,x)));
    return 0;
}

C - Shorten Diameter

\(n\) 很小可以瞎搞。

\(k\) 为偶数,暴力枚举每一种有根树,将深度超过 \(k/2\) 的点删掉,最后取个 \(\min\) 就好了。

\(k\) 为奇数,那就枚举两个点,分别求,然后相加就好了。

点击查看代码
const int N=1e6+5;

int n,k,tot,ans;
vector<int>G[N];

inline void dfs(int u,int f,int step){
    tot++;
    if(step==0)
        return;
    for(auto v:G[u])
        if(v!=f)
            dfs(v,u,step-1);
}

signed main(){
    n=read();k=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        G[u].eb(v);G[v].eb(u);
    }
    if(k&1){
        for(int u=1;u<=n;u++)
            for(auto v:G[u]){
                tot=0;
                dfs(u,v,k/2);
                dfs(v,u,k/2);
                ans=max(ans,tot);
            }
    }else{
        for(int i=1;i<=n;i++){
            tot=0;
            dfs(i,0,k/2);
            ans=max(ans,tot);
        }
    }
    write(n-ans);
    return 0;
}

D - Arrays and Palindrome

构造好题。

如果把回文的对应相等的关系当成连边,就相当于希望这个东西连成一个联通块。

首先不难发现,每次连的边数是 \(\sum_{i=1}^{M} \left\lfloor \frac{a_i}{2} \right \rfloor\) 的。如果给出的 \(a_i\) 中存在三个及以上的奇数,那么就一定不可能了。

因为每有两个奇数,连出来的边数就会减少 \(1\)。连成一个联通块至少要是一棵树才行。

先从特殊的情况考虑:\(M=1\) 的时候,发现只需要令 \(a_1=N,b_1=1,b_2=N-1\) 就行了。

然后考虑这样构造:首先将所有奇数分别放在开头结尾,然后这样构成 \(a\) 数列,

再让开头 \(+1\),结尾 \(-1\),就能构造出来合法的序列了。

不难发现,这样构造,会让中间的边正好错开,前后两个的边也是错开的,边数恰为 \(n-1\),正好构成一棵树。

点击查看代码
const int N=1e4+5;
int n,m,a[N],cnt[N],ans[N];

signed main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        a[i]=read();
        cnt[a[i]&1]++;
    }
    if(m==1){
        if(a[1]==1)
            cout<<"1\n1\n1\n";
        else
            cout<<a[1]<<endl<<2<<endl<<1<<' '<<a[1]-1<<endl;
        return 0;
    }
    if(cnt[1]>2)
        return puts("Impossible"),0;
    sort(a+1,a+m+1,[](int i,int j){return i%2>j%2;});
    write(a[1]);putchar(' ');
    for(int i=3;i<=m;i++)
        write(a[i]),putchar(' ');
    write(a[2]);puts("");ans[++ans[0]]=a[1]+1;
    for(int i=3;i<=m;i++)
        ans[++ans[0]]=a[i];
    if(a[2]>1)
        ans[++ans[0]]=a[2]-1;
    write(ans[0]);puts("");
    for(int i=1;i<=ans[0];i++)
        write(ans[i]),putchar(' ');
    return 0;
}

E - BBQ Hard

观察到值域很小,考虑从这方面下手。

发现题目给的式子很像格路计数,将其转化到坐标系上,即对于每个 \((i,j)\) 都是从 \((0,0)\) 走到 \((a_i+a_j,b_i+b_j)\)

但是 dp 完之后统计答案依旧是 \(n^2\) 的,因为对于每个 \((i,j)\) 都要计算一次。

考虑将点平移,即每个路径变为从 \((-a_i,-b_i)\)\((a_j,b_j)\),现在起点只与 \(i\) 相关,终点只与 \(j\) 相关。

将所有的 \(dp_{-a_i,-b_i}\) 加一,dp完之后 \(dp_{i,j}\) 救变成了 \((i,j)\) 到所有 \((-a_i,-b_i)\) 的路径数量之和。所以答案就是对所有 \(dp_{a_i,b_i}\) 求和。

注意 \(i=j\)\(i<j\) 的情况要减去。\(i=j\) 的部分可以直接组合计算,\(i<j\) 的部分总答案取半即可。

点击查看代码
const int N=2e5+5;
const int M=4e3+5;
int n,fac[N],inv[N],a[N],b[N],f[M][M],ans;

inline void init(int lim){
    fac[0]=1;
    for(int i=1;i<=lim;i++)
        fac[i]=mul(fac[i-1],i);
    inv[lim]=Inv(fac[lim]);
    for(int i=lim;i;i--)
        inv[i-1]=mul(inv[i],i);
}

inline int C(int x,int y){
    return mul(fac[x],mul(inv[y],inv[x-y]));
}

signed main(){
    init(N-5);
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read();b[i]=read();
        f[2001-a[i]][2001-b[i]]++;
    }
    for(int i=1;i<=4001;i++)
        for(int j=1;j<=4001;j++)
            add(f[i][j],inc(f[i-1][j],f[i][j-1]));
    for(int i=1;i<=n;i++){
        int x=f[a[i]+2001][b[i]+2001],y=C(2ll*(a[i]+b[i]),2ll*a[i]);
        add(ans,x);sub(ans,y);
    }
    write(mul(ans,Inv(2)));
    return 0;
}

F - Wide Swap

考虑构造序列 \(id\) 使得 \(id_{P_i}=i\),变成交换相邻的差值 \(\geq k\) 的数。

先考虑暴力做法,按照冒泡排序,把大的尽可能往右挪。

然后把冒泡排序改为归并排序。一个右边的数能比左边的数先进行归并就要保证它加 \(k\) 小于等于左边的数的后缀最小值即可。

每次归并时记录左边的后缀最小值。

点击查看代码
const int N=1e6+5;
int n,k,a[N],id[N],Min[N],b[N];

inline void merge(int l,int r){
    if(l==r)
        return;
    int mid=(l+r)>>1;
    merge(l,mid);merge(mid+1,r);
    int num=N,now1=l,now2=mid+1,cnt=l;
    for(int i=mid;i>=l;i--){
        num=min(num,a[i]);
        Min[i]=num;
    }
    while(now1<=mid and now2<=r){
        if(Min[now1]>=a[now2]+k)
            b[cnt++]=a[now2++];
        else
            b[cnt++]=a[now1++];
    }
    while(now1<=mid)
        b[cnt++]=a[now1++];
    while(now2<=r)
        b[cnt++]=a[now2++];
    for(int i=l;i<=r;i++)
        a[i]=b[i];
}

signed main(){
    n=read();k=read();
    for(int i=1;i<=n;i++)
        a[read()]=i;
    merge(1,n);
    for(int i=1;i<=n;i++)
        id[a[i]]=i;
    for(int i=1;i<=n;i++)
        write(id[i]),puts("");
    return 0;
}

AGC002


A - Range Product

如果区间包含0就输出Zero,如果负数的个数是奇数个就输出Negative,否则输出Positive

点击查看代码
signed main(){
    int a=read(),b=read();
    if(a>0)
        puts("Positive");
    else if(b>=0)
        puts("Zero");
    else if((a&1)^(b&1))
        puts("Positive");
    else
        puts("Negative");
    return 0;
}

B - Box and Ball

\(g_i\) 表示第 \(i\) 个盒子中球的数量,\(f_i\) 表示第 \(i\) 个盒子是否有红球,模拟即可。

点击查看代码
const int N=1e6+5;
int n,m,f[N],tot[N],x,y,ans;

signed main(){
    n=read();m=read();
    fill(tot+1,tot+1+n,1);f[1]=1;
    for(int i=1;i<=m;i++){
        x=read();y=read();
        tot[x]--;tot[y]++;
        if(f[x]==1){
            f[y]=1;
            if(!tot[x])
                f[x]=0;
        }
    }
    for(int i=1;i<=n;i++)
        if(f[i])
            ans++;
    write(ans);
    return 0;
}

C - Knot Puzzle

若可以找到连续的两段长度 \(\geq L\) 的绳子那么就可以全部解开。将左边的绳结从左往右解开,右边的绳结从右往左解开,最后将这个绳结解开。

点击查看代码
const int N=1e6+5;
int n,l,a[N];

signed main(){
    n=read();l=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    int x=0;
    for(int i=1;i<n;i++)
        if(a[i]+a[i+1]>=l)
            x=i;
    if(x==0){
        puts("Impossible");
        return 0;
    }
    puts("Possible");
    for(int i=1;i<x;i++)
        write(i),puts("");
    for(int i=n-1;i>x;i--)
        write(i),puts("");
    write(x);
    return 0;
}

D - Stamp Rally

对图建一个 \(kruskal\) 重构树。

因为每一条树枝上的边权是单调的,那么可以倍增来快速找到最大的不超过某个值的最小位置。

用二分答案。对于每个 \(x\)\(y\),分别向上跳到点权是大于二分的值为止,然后向下子树中叶节点个数一加就是经过点的个数,和 \(z\) 比一下即可实现。

注意 \(x,y\) 跳到一起的贡献为 \(1\)

点击查看代码
const int N=1e6+5;
int n,m,fa[N],tot,val[N],f[N][18],cnt,ans,siz[N],q;
vector<int>G[N];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
inline void dfs(int u,int fa){
    f[u][0]=fa;
    for(auto v:G[u])
        if(v!=fa)
            dfs(v,u),siz[u]+=siz[v];
}

inline int check(int x,int y,int mid){
    for(int i=17;i>=0;i--){
        if(val[f[x][i]]<=mid)
            x=f[x][i];
        if(val[f[y][i]]<=mid)
            y=f[y][i];
    }
    if(x==y)
        return siz[x];
    else
        return siz[x]+siz[y];
}

signed main(){
    cnt=n=read();m=read();
    for(int i=1;i<2*n;i++)
        fa[i]=i;
    for(int i=1;i<=n;i++)
        siz[i]=1;
    val[0]=inf;
    for(int i=1;i<=m;i++){
        int x=read(),y=read();
        if(cnt==2*n-1)
            continue;
        int u=find(x),v=find(y);
        if(u!=v){
            fa[u]=fa[v]=++cnt;
            val[cnt]=i;
            G[cnt].eb(u);G[cnt].eb(v);
        }
    }
    dfs(cnt,0);
    for(int j=1;j<=17;j++)
        for(int i=1;i<=cnt;i++)
            f[i][j]=f[f[i][j-1]][j-1];
    q=read();
    for(int i=1;i<=q;i++){
        int x=read(),y=read(),z=read();
        int l=1,r=m,mid;ans=0;
        while(l<=r){
            mid=(l+r)>>1;
            if(check(x,y,mid)>=z)
                ans=mid,r=mid-1;
            else
                l=mid+1;
        }
        write(ans);puts("");
    }
    return 0;
}

E - Candy Piles

先将 \(\{a\}\) 从大到小排序,并对其画网格图。

一开始在原点,对于取走石子最多的一堆,实际就是往右走一步;对于取走每堆石子取走 \(1\) 个,实际就是往上走一步。当走到网格图的边界时,所有石子刚好被取完。

所以题目转化为在网格图上轮流选择向上或向右走,谁走到边界谁就输。

显然边界上的点都是必败点。对于任意一个不在边界上的点,如果它的上面和右面都是必败点,那么这个点一定是必胜点。如果它的上面和右面有一个是必胜点,那它就是必败点。

将整个网格图构造出来复杂度太大,所以考虑找规律:

发现除了边界外,同一对角线上的点全是相同类型。

可以通过算出原点最右上方且不在边界上的点的类型,来知道原点是必胜点还是必败点。

找到以原点为左下角的最大正方形,记其右上角为 \(A\) 点。当 \(A\) 到最上面且不在边界上的点的距离和最右面且不在边界上的点的距离其中一个为奇数时,这个点为必败点,反之这个点为必胜点。

点击查看代码
const int N=1e6+5;
int n,a[N];

signed main(){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    sort(a+1,a+1+n,greater<int>());
    for(int i=1;i<=n;i++)
        if(i+1>a[i+1]){
            int j=0;
            while(a[j+i+1]==i)
                j++;
            if(((a[i]-i)&1) or (j&1))
                puts("First");
            else
                puts("Second");
            break;
        }
    return 0;
}

F - Leftmost Ball

数数好题。

考虑最后的颜色序列,一共有 \(k\) 个白球,\(n\) 种其他颜色的球各 \(n-1\) 个。显然序列合法当且仅当任意前缀中白球的个数均大等于其他颜色的种类数。

考虑 dp。设 \(f_{i,j}\) 表示已经放了 \(i\) 个白球和 \(j\) 种其他的颜色的可行方案数,其中 \(j\leq i\)

转移时考虑合法序列的从左到右的第一个空位放球的颜色,有放白球和放其他颜色球的两种情况。

若放白球,则直接从 \(f_{i-1,j}\) 转移,因为是在第一个空位放置白球,转移过来必定合法;

若放其他颜色球,则从 \(f_{i,j-1}\) 转移。考虑转移过来的方案数。

先在剩下 \(n-j+1\) 种没有放进序列中的颜色中任取一个作为当前放入的颜色,然后将一个球放到当前从左到右第一个空位,因为之前的白球都是放在这个空位前面的,所以必然合法。剩下的 \(k-2\) 个球就在剩下的空位中任意放,由于还剩下 \(n\times k−i−(j−1)\times(k−1)−1\) 个空,所以方案数为 \(\binom{n\times k−i−(j−1)\times(k−1)−1}{k-2}\)

初始化 \(dp_{0,0}=1\)

所以 dp 转移式为:

\[f_{i,j}=\begin{cases}1&(i=j=0)\\f_{i-1,j}+f_{i,j-1}\times(n-j+1)\times\dbinom{n\times k−i−(j−1)\times(k−1)−1}{k-2}&(\text{otherwise})\end{cases} \]

注意特判 \(k=1\) 的情况。

点击查看代码
const int N=2e3+5;
int n,k,f[N][N],fac[N*N],inv[N*N];

inline void init(){
    int w=N*N-5;fac[0]=1;
    for(int i=1;i<=w;i++)
        fac[i]=mul(fac[i-1],i);
    inv[w]=Inv(fac[w]);
    for(int i=w;i;i--)
        inv[i-1]=mul(inv[i],i);
}

inline int C(int x,int y){
    return mul(fac[x],mul(inv[x-y],inv[y]));
}

signed main(){
    init();
    n=read();k=read();
    if(k==1){
        write(1);
        return 0;
    }
    f[0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=i;j++)
            f[i][j]=inc(f[i-1][j],mul(f[i][j-1],mul(n-j+1,C(n*k-i-(j-1)*(k-1)-1,k-2))));
    write(f[n][n]);
    return 0;
}

AGC003


A - Wanna go back home

若两个相反方向都同时出现或者同时不出现,则可以到达,否则不行。

点击查看代码
const int N=1e6+5;
char s[N];

int a,b,c,d;

signed main(){
    scanf("%s",s);
    for(auto i:s){
        if(i=='S')
            a=1;
        if(i=='N')
            b=1;
        if(i=='E')
            c=1;
        if(i=='W')
            d=1;
    }
    if((a==b)and(c==d))
        puts("Yes");
    else
        puts("No");
    return 0;
}

B - Simplified mahjong

贪心,先与自己匹配,再与自己 +1 的数匹配。

点击查看代码
const int N=1e6+5;
int n,a[N],ans;

signed main(){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    for(int i=1;i<=n;i++){
        ans+=a[i]/2;a[i]%=2;
        if(a[i] and a[i+1])
            a[i+1]--,ans++;
    }
    write(ans);
    return 0;
}

C - BBuBBBlesort!

操作 2 相当于交换 \(a_i\)\(a_{i+2}\) 的值,且这个操作的贡献为 \(0\),所以要使这个操作的次数尽可能多。

发现经过操作 2 后可以使所有奇数位置上的数和所有偶数位置上的数排好序。但是无法将整个序列排好序,因为如果奇数位置上的数无法排序到偶数位置上。

这时候使用操作 1。 显然两个数的位置的奇偶性的错误是成对出现的。可以先用操作 2 让它们相邻再用操作 1 交换它们,在通过操作 1 将它们放到正确的位置上。

所以统计出位置和目标位置奇偶性不同的数的数量 \(cnt\),答案就为 \(\frac{cnt}{2}\)

点击查看代码
const int N=1e6+5;
int n,a[N],b[N];

signed main(){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=b[i]=read();
    sort(b+1,b+1+n);
    for(int i=1;i<=n;i++)
        a[i]=lower_bound(b+1,b+1+n,a[i])-b;
    int ans=0;
    for(int i=1;i<=n;i++)
        if((a[i]&1)!=(i&1))
            ans++;
    write(ans>>1);
    return 0;
}

D - Anticube

数论分讨好题。

首先可以把对答案没有影响的立方因子全部筛掉。

剩下的数都可以用 \(x^2\times y\) 表示,其中 \(y\) 不含平方因子。

\(a_i\)\(s_i\) 筛掉立方因子后的结果,那么显然有 \(\min\{x,y\}\leq\sqrt[3]{a_i}\)

枚举 \(j=1\sim\sqrt[3]{a_i}\),对于每一个 \(j\) 将其作为 \(x\)\(y\) 分别讨论。若 \(a_i\) 不为 \(1\),最后只要满足求出的 \(x\) 最大,就可以保证 \(y\) 中不含平方因子。因为若 \(x\) 中含平方因子,都可以有一种方案,使得 \(y\) 比原来的更大。

对于每一个 \(a_i\) 所求出来的数对 \(\left(x,y\right)\),都会有数对 \(\left(y,x\right)\) 与其冲突。

所以可以用 map 统计每个数对出现的次数,最后答案累加上 \(\max\{\text{cnt}_{(x,y)},\text{cnt}_{(y,x)}\}\) 即可。

注意讨论 \(x=y=1\) 的情况。

点击查看代码
const int N=1e6+5;
int n,a[N],ans;

pii p[N];
map<pii,int>cnt;

signed main(){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    for(int i=1;i<=n;i++)
        for(int j=2;j*j*j<=a[i];j++)
            while((a[i]%(j*j*j))==0)
                a[i]/=(j*j*j);
    for(int i=1;i<=n;i++){
        int x=0,y=1e9,X=0,Y=1e9;
        for(int j=1;j*j*j<=a[i];j++)
            if((a[i]%(j*j))==0)
                x=j,y=a[i]/(j*j);
        for(int j=1;j*j*j<=a[i];j++)
            if((a[i]%j)==0){
                X=(int)sqrt(a[i]/j);Y=j;
                if(X*X!=a[i]/j)
                    X=0,Y=1e9;
                else
                    break;
            }
        p[i]=mp(max(x,X),min(y,Y));
        cnt[p[i]]++;
    }
    for(auto i:cnt)
        if(i.fi.fi==i.fi.se)
            ans++;
        else{
            ans+=max(i.se,cnt[mp(i.fi.se,i.fi.fi)]);
            cnt[mp(i.fi.se,i.fi.fi)]=cnt[i.fi]=0;
        }
    write(ans);
    return 0;
}

E - Sequential operations on Sequence

如果一个 \(a_{i+1}<a_i\) 那么 \(a_{i+1}\) 显然是没有意义的。因此可以在读入时维护一个单调栈,使得最后的操作序列单调递增。

考虑计算第 \(i\) 次操作过程。前面的一大堆都是上一次操作所得答案的 \(\lfloor\frac{a_i}{a_{i-1}}\rfloor\) 倍,所以对于一个操作,只要单独处理 \(a_i \bmod a_{i-1}\) 那一部分的答案。前面的部分直接乘上一个系数就好了。

考虑如何处理单独的部分,是否也是某个东西重复出现。如果有一个 \(a_j<x\) 并且 \(x<a_{j+1}\) 那么 \(a_j\)\(x\)\(x\)\(a_{j+1}\) 的关系也是完全一样的。都是前者重复出现然后取一个长度为后者的前缀,所以将 \(x\) 模上 \(a_j\) 继续递归就好了。

这个过程显然要倒着来做,因为正着做不晓得当前操作的系数是多少。

递归到底后就是 \(\left[1,x\right]\) 区间加,可以用差分解决。二分 \(x\) 一个 \(\log\),每次递归至少减半也带一个 \(\log\) 所以最终时间复杂度为 \(O(n\log n)\)

点击查看代码
const int N=1e6+5;
int n,m,a[N],f[N],z[N];

inline int find(int x,int y){
    return upper_bound(a+1,a+x+1,y)-a-1;
}

inline void solve(int x,int y,int w){
    int t=find(x-1,y);if(!t) {z[y]+=w;return;}
    f[t]+=y/a[t]*w;int r=y%a[t];if(!r) return;solve(t,r,w);
}

signed main(){
    n=read();m=read();a[++a[0]]=n;
    for(int i=1;i<=m;i++){
        int x=read();
        while(a[0] and x<=a[a[0]])
            a[0]--;
        a[++a[0]]=x;
    }
    f[a[0]]=1;
    for(int i=a[0];i;--i)
        solve(i,a[i],f[i]);
    for(int i=n;i;i--)
        z[i]+=z[i+1];
    for(int i=1;i<=n;i++)
        write(z[i]),puts("");
    return 0;
}


F - Fraction of Fractal

画画题。

因为保证黑格四联通,可以分三种独立的情况:

  1. 上下左右拼接都可以形成一个联通块,此时显然答案为 \(1\)
  2. 上下左右都不可以拼接成一个联通块,此时显然答案为 \(c^{k-1}\)。其中 \(c\) 为图中黑点的数量。
  3. 上下或左右可以拼接成联通块。

考虑情况 3。上下和左右是等价的,所以只讨论左右拼接的情况。

画图容易发现联通块个数可以表示为原图的黑点个数减去左右相邻两个都是黑格的对数。

然后把这个图当成原图,再放入一开始给出的图中,得到的新图的联通块个数也可以用上面这个表达式表示。

所以答案就是 \(k-1\) 分形图中黑点个数减去左右相邻两个都是黑格的对数。

黑点个数为 \(c^{k-1}\),但是相邻两个都是黑点的对数较为难求。

继续观察画出来的图,发现在每个块里头都会有原图的相邻黑点对数的贡献。然后在边界上也会多出一些贡献。多出来的贡献恰好是相邻黑点对数与左右拼接边界上相邻黑点对数的积。

现在只需算左右拼接边界上的相邻黑点对数。只有可能在原来左右拼接边界相邻的行代表的一串图的边界才可能有贡献。所以这个点对数就等于原图的这个点对数的平方。

\(a\) 为相邻黑点对数,\(b\) 为左右拼接边界相邻的黑点对数,\(c\) 为黑格数,那么上面的关系可以写成:

\[{\begin{bmatrix}{c}&{a}\\{0}&{b}\end{bmatrix}}^{k-1} \]

点击查看代码
const int W=3;

struct mat{
    int a[W][W];
    mat(){memset(a,0,sizeof(a));}
    inline void init(){
        for(int i=1;i<=W;i++)
            a[i][i]=1;
    }
};

inline mat Mul(mat x,mat y){
    mat res;
    for(int i=1;i<W;i++)
        for(int j=1;j<W;j++)
            for(int k=1;k<W;k++)
                add(res.a[i][j],mul(x.a[i][k],y.a[k][j]));
    return res;
}

inline mat Qpow(mat a,int k){
    mat res;res.init();
    while(k){
        if(k&1)
            res=Mul(res,a);
        a=Mul(a,a);
        k>>=1;
    }
    return res;
}

const int N=1e3+5;

int n,m,k;
int cnt,tot[2],uside[2];
char s[N][N];
bool tag;

signed main(){
    n=read();m=read();k=read();
    for(int i=1;i<=n;i++)
        scanf("%s",s[i]+1);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(s[i][j]=='#'){
                cnt++;
                if(j>1)
                    tot[0]+=(s[i][j-1]=='#');
                if(i>1)
                    tot[1]+=(s[i-1][j]=='#');
            }
    for(int i=1;i<=n;i++)
        uside[0]+=(s[i][1]=='#' and s[i][m]=='#');
    for(int i=1;i<=m;i++)
        uside[1]+=(s[1][i]=='#' and s[n][i]=='#');
    if(uside[0] and uside[1]){
        puts("1");
        return 0;
    }
    if(!uside[0] and !uside[1]){
        write(qpow(cnt,k-1));
        return 0;
    }
    tag=uside[1];
    mat a,b;
    a.a[1][1]=1;a.a[1][2]=1;
    b.a[1][1]=cnt;b.a[2][1]=-tot[tag]+P;b.a[2][2]=uside[tag];
    mat ans=Mul(a,Qpow(b,k-1));
    write(ans.a[1][1]);
    return 0;
}

AGC004


A - Wanna go back home

有一条边是偶数的时候,直接从那条边的正中间切过去,那么差就为 \(0\)

当每条边都是奇数时,从接近中间处切一刀,此时差的面积为 \(a\times b\),为了让体积最小,所以选取最短的两条边相乘得到体积。

点击查看代码
int a,b,c;

signed main(){
    a=read();b=read();c=read();
    if(a&1 and b&1 and c&1){
        write(min(min(a*b,b*c),a*c));
        return 0;
    }
    puts("0");
    return 0;
}

B - Colorful Slimes

\(f_{i,j}\) 为表示通过 \(j\) 次变换凑出颜色 \(i\) 的最小代价。

转移很显然:

\[f_{i,j}=\min\{dp_{lst,j-1},a_i\} \]

其中 \(lst\) 为环上的前驱。

所以答案为:

\[\min\limits_{j=0}^{n-1}\{j\times x+\sum\limits_{i=1}^{n}f_{i,j}\} \]

点击查看代码
const int N=2e3+5;
int n,x,f[N][N],a[N],ans=inf;

signed main(){
    n=read();x=read();
    for(int i=1;i<=n;i++)
        f[i][0]=a[i]=read();
    for(int j=1;j<n;j++)
        for(int i=1;i<=n;i++){
            int lst=i==1?n:i-1;
            f[i][j]=min(a[i],f[lst][j-1]);
        }
    for(int j=0;j<n;j++){
        int now=j*x;
        for(int i=1;i<=n;i++)
            now+=f[i][j];
        ans=min(now,ans);
    }
    write(ans);
    return 0;
}

C - AND Grid

喵喵构造题。

让第一张图,第一列全部是 #,第二张图最后一列全部是 #

然后对于中间的部分,奇数行在第一张图全部染色,偶数行在第二张图全部染色。

然后对于原图中的 #,在另一张图也染上色。

点击查看代码
const int N=2e3+5;
int n,m;
char a[N][N],b[N][N];

signed main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            char ch=getchar();
            while(ch!='.' and ch!='#')
                ch=getchar();
            a[i][j]=b[i][j]=ch;
        }
    for(int i=1;i<=n;i++){
        a[i][1]='#';b[i][m]='#';
        if(i&1)
            for(int j=2;j<=m-1;j++) a[i][j]='#';
        else
            for(int j=2;j<=m-1;j++) b[i][j]='#';
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)
            putchar(a[i][j]);
        puts("");
    }
    puts("");
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)
            putchar(b[i][j]);
        puts("");
    }
    return 0;
}

D - Teleporter

显然 \(1\) 号点要指向自己。

然后简单贪心从叶节点开始每走 \(k\) 层都往 \(1\) 号点连边。

点击查看代码
const int N=1e5+5;
int n,k,a[N],ans;

vector<int>G[N];

inline int dfs(int now,int fa,int dep){
    int res=dep;
    for(auto v:G[now])
        if(v!=fa){
            res=max(res,dfs(v,now,dep+1));
        }
    if(a[now]!=1 and res-dep==k-1){
        ans++;
        return 0;
    }
    return res;
}

signed main(){
    n=read();k=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    if(a[1]!=1)
        ans=a[1]=1;
    for(int i=2;i<=n;i++){
        G[a[i]].eb(i);
        G[i].eb(a[i]);
    }
    dfs(1,0,1);
    write(ans);
    return 0;
}


E - Salvage Robots

机器人挪动其实等价于让出口移动,出口自带一个框,出过框的机器人就死了,终点抵达的机器人就出去了。

定义 \(f_{l,r,u,d}\) 表示出口 \(E\) 向四个方向所能抵达的最远的位置。显然,在最优情况下必然存在出口跑成一个矩形。

考虑这么跑所带来的影响。每往上扩展一格,需要在原矩形的基础上增加一行,行的范围是矩形的宽度,往下同理。往左扩展一格,需要在原矩形的基础上增加一列,列的范围是矩形的高度,往右同理。

每扩展一些范围,就要将扩展的范围里面包含的机器人数量加进来。求扩展范围内包含多少机器人,需要一个基于行的前缀和以及基于列的前缀和来优化一下。

点击查看代码
const int N=105;
int n,m,x,y,v[N][N][2];
short f[N][N][N][N];

signed main(){
    n=read();m=read();
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            char s=getchar();
            while(s!='o' and s!='.' and s!='E')
                s=getchar();
            if(s=='E'){
                x=i ;
                y=j;
            }else if(s=='o'){
                v[i][j][0]=v[i-1][j][0]+1;
                v[i][j][1]=v[i][j-1][1]+1;
            }else{
                v[i][j][0]=v[i-1][j][0];
                v[i][j][1]=v[i][j-1][1];
            }
        }
    }
    int ans=0;
    for(int l=0;l<=y-1;l++){
        for(int r=0;r<=m-y;r++){
            for(int u=0;u<=x-1;u++){
                for(int d=0;d<=n-x;d++){
                    ans=max(ans,(int)f[l][r][u][d]);
                    if(l+r<y-1)
                        f[l+1][r][u][d]=max((int)f[l+1][r][u][d],(int)f[l][r][u][d]+v[min(x+d,n-u)][y-l-1][0]-v[max(x-u-1,d)][y-l-1][0]);
                    if(l+r<m-y)
                        f[l][r+1][u][d]=max((int)f[l][r+1][u][d],(int)f[l][r][u][d]+v[min(x+d,n-u)][y+r+1][0]-v[max(x-u-1,d)][y+r+1][0]);
                    if(u+d<x-1)
                        f[l][r][u+1][d]=max((int)f[l][r][u+1][d],(int)f[l][r][u][d]+v[x-u-1][min(y+r,m-l)][1]-v[x-u-1][max(y-l-1,r)][1]);
                    if(u+d<n-x)
                        f[l][r][u][d+1]=max((int)f[l][r][u][d+1],(int)f[l][r][u][d]+v[x+d+1][min(y+r,m-l)][1]-v[x+d+1][max(y-l-1,r)][1]);
                }
            }
        }
    }
    write(ans);
    return 0;
}

F - Namori

神仙题。

首先有 \(n-1\leq m\leq n\),所以图是树或者基环树。

考虑树怎么做。进行一次转化。因为树是一个二分图,于是就进行一次二分图染色。不难发现新图上两端颜色不同当且仅当原图两端颜色相同,所以可以把操作从同色翻转变成异色反转,也就是进行的操作变成了在新图上移动黑色点。

然后问题就变成了一棵树,其中树上有一些位置是黑色点,一些位置是白色点。每次要移动一个黑色点到相邻的白色点,最后要让所有黑色点归位。

如果黑白点数的量不同,显然无解。

首先答案是有下界的。随意取一个根,对于一个子树,设它内部的黑点数减去白点数量为 \(a\),那么这个子树向上的边至少会被经过 \(a\) 次。现在证明下界可以取到。

首先如果不考虑同一个点上不能有两个黑色点,那么移动的次数相当于是一个最小权匹配。这个东西对于一条边,它被经过的次数显然可以是它一边的黑色点个数与白色点个数的差值。因为剩下的可以在内部匹配。

然后考虑黑色点不能覆盖,那么对于之前的一种 \(u\to v\) 的移动方案,假设中间存在第一个 $w$ 在 \(u\to v\) 那么我们可以让 \(w\to v\) 并且让 \(u\to w\)。这样不断调整,一定可以达成 \(u\) 上的黑点到达 \(v\) 上的目的。

所以答案为 \(\sum\lvert a_i\rvert\)

然后考虑基环树的情况。

对于奇环,环两边的颜色是相同的。环边操作一次就会让黑色点增加 \(2\) 或减少 \(2\)。在这种情况下,如果黑白色点的差是偶数就一定可以通过操作这个白边来让整个图有一个解,并且这种边的唯一用处就是用来增加点。

所以可以直接给这条基环边的两侧加上全图黑色点与白色点的差除以 \(2\),然后跑树的做法。

对于偶环,还两边的颜色是不同的。设环边是 \(u\to v\)\(x\) 为这个边的操作次数,若 \(x<0\) 则为 \(v\to u\)

考虑环中边与环的顶点分成的这两部分,一部分到父亲的边一定会多向下操作 \(x\) 次,另一部分到父亲的边一定会多向上操作 \(x\) 次。

于是设把边 \(i\) 断掉后每个点跑出的答案是 \(a_i\),那么答案为:

\[\sum\limits_{i\in u}\lvert a_i-x\rvert+\sum\limits_{i\in v}\lvert -a_i-x\rvert+\lvert -x\rvert \]

这个东西就是在数轴上一些点到 \(x\) 的距离和的最小值。可以确定出 \(x\) 的值。

点击查看代码
const int N=1e6+5;
int n,m,fa[N],q[N],depth[N],sum,ans;

bool odd,even,vis[N];
int lu,lv;

vector<int>G[N];

inline void dfs(int u,int f,int c){
    fa[u]=f;q[u]=c;sum+=c;
    for(auto v:G[u])
        if(v!=f){
            if(q[v]){
                if(q[u]*q[v]>0)
                    odd=true;
                else
                    even=true;
                lu=u;lv=v;
            }else
                dfs(v,u,-c);
        }
}

inline void dfs2(int u,int f){
    depth[u]=depth[f]+1;
    for(auto v:G[u])
        if(v!=f and !depth[v]){
            dfs2(v,u);
            q[u]+=q[v];
        }
}

signed main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        int u=read(),v=read();
        G[u].eb(v);G[v].eb(u);
    }
    dfs(1,0,1);
    if(sum&1){
        puts("-1");
        return 0;
    }
    if(odd){
        q[lu]-=sum>>1;q[lv]-=sum>>1;
        ans+=abs(sum>>1);sum=0;
    }
    if(sum){
        puts("-1");
        return 0;
    }
    dfs2(1,0);
    if(even){
        vector<int>res;
        if(depth[lu]<depth[lv])
            swap(lu,lv);
        while(depth[lu]!=depth[lv]){
            vis[lu]=true;
            res.eb(q[lu]);
            lu=fa[lu];
        }
        res.eb(0);
        sort(res.begin(),res.end());
        int t=res[res.size()>>1];
        for(auto i:res)
            ans+=abs(i-t);
    }
    for(int i=1;i<=n;i++)
        if(!vis[i])
            ans+=abs(q[i]);
    write(ans);
    return 0;
}

AGC005


A - STring

用栈模拟即可。

点击查看代码
const int N=1e6+5;

char s[N];

stack<int>stk,stk2;

signed main(){
    scanf("%s",s+1);
    int len=strlen(s+1);
    for(int i=1;i<=len;i++){
        if(s[i]=='S')
            stk.push('S');
        else if(s[i]=='T' and !stk.empty())
            stk.pop();
        else
            stk2.push('T');
    }
    write(stk.size()+stk2.size());
    return 0;
}

B - Minimum Sum

考虑每一个数值对答案的贡献,用单调栈维护每个数左右两边第一个比它小的位置。

点击查看代码
const int N=1e6+5;
int n,a[N],l[N],r[N],ans;

stack<int>stk;

signed main(){
    n=read();
    for(int i=1;i<=n;i++)
        a[i]=read();
    fill(r+1,r+1+n,n+1);
    for(int i=1;i<=n;i++){
        while(stk.size() and a[stk.top()]>a[i]){
            r[stk.top()]=i;
            stk.pop();
        }
        l[i]=stk.size()?stk.top():0;
        stk.push(i);
    }
    for(int i=1;i<=n;i++)
        ans+=(r[i]-i)*(i-l[i])*a[i];
    write(ans);
    return 0;
}

C - Tree Restoring

直径两端的 \(a\) 肯定是最大的,由两段向中间递减。

所以开个桶,第 \(i\) 位记录 \(a\) 值为 \(i\) 的有几个,然后减去构造直径所需的点,如果构造不了直径或存在比直径上最小值更小的点就无法构造出这棵树。

点击查看代码
const int N=1e6+5;
int n,a[N],vis[N],mx;

inline bool check(){
    int mid=mx/2+1;
    for(int i=mid;i<=mx;i++){
        vis[i]-=2;
        if(vis[i]<0)
            return false;
    }
    if(!(mx&1)){
        vis[mid-1]--;
        if(vis[mid-1]<0)
            return false;
        mid--;
    }
    for(int i=1;i<=mid;i++)
        if(vis[i])
            return false;
    return true;
}

signed main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
        vis[a[i]]++;
        mx=max(mx,a[i]);
    }
    bool b=check();
    puts(b?"Possible":"Impossible");
    return 0;
}

D - ~K Perm Counting

将排列转化到网格图上:

image by Dreamunk.

就等于在这样的棋盘格子内放 \(n\) 个互不攻击的車,且阴影格子不能放的方案数。上图是 \(n=7,k=2\) 的情况。

考虑容斥。设 \(f_i\) 为往阴影里放 \(i\) 个車,剩下随便放的方案数,那么答案为:

\[\sum\limits_{i=0}^{n}(-1)^{n}f_i(n-i)! \]

现在考虑求 \(f\)。发现同一行同一列的阴影格子还是不能同选,把不能同选的格子连上边。很明显这个图就是若干条链。

考虑把这些链连起来。转为求从这一排结点中选 \(i\) 个,拼接的位置可以相邻,其他位置不能相邻的方案数。简单 dp。

点击查看代码
const int N=5e3+5;
int n,k,t,fac[N],inv[N],f[N][N],a[N],ans;

inline void init(int lim){
    fac[0]=1;
    for(int i=1;i<=lim;i++)
        fac[i]=mul(fac[i-1],i);
}

signed main(){
    n=read();k=read();
    init(n);
    a[t=0]=1;
    for(int i=1;i<=(n-k)%k;i++)
        a[t+=(n-k)/k+1]=a[t+=(n-k)/k+1]=1;
    for(int i=1;i<=k-(n-k)%k;i++)
        a[t+=(n-k)/k]=a[t+=(n-k)/k]=1;
    f[0][0]=1;
    for(int i=1;i<=t;i++)
        for(int j=0;j<=n;j++)
            f[i][j]=inc(f[i-1][j],(j?f[i-1-(!a[i-1])][j-1]:0));
    for(int j=0;j<=n;j++)
        add(ans,mul(mul(j&1?P-1:1,f[t][j]),fac[n-j]));
        write(ans);
    return 0;
}

本文作者:QcpyWcpyQ

本文链接:https://www.cnblogs.com/QcpyWcpyQ/p/-/AGC_solutions

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   QcpyWcpyQ  阅读(29)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起