成為 AGC 皇帝,從你他她它祂牠鉈做起

QcpyWcpyQ發表於2024-04-09

AGC001


A - BBQ Easy

  • AtCoder & Luogu

簡單貪心,兩個兩個一起選。

點選檢視程式碼
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

  • AtCoder & Luogu

發現與輾轉相除類似,根據幾何意義畫圖得出答案。

點選檢視程式碼
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

  • AtCoder & Luogu

\(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

  • AtCoder & Luogu

構造好題。

如果我們把迴文的對應相等的關係當成連邊,我們就相當於希望這個東西連成一個聯通塊。

首先不難發現,我們每次連的邊數是 \(\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

  • AtCoder & Luogu

觀察到值域很小,考慮從這方面下手。

發現題目給的式子很像格路計數,將其轉化到座標系上,即對於每個 \((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

  • AtCoder & Luogu

考慮構造序列 \(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

  • AtCoder & Luogu

如果區間包含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

  • AtCoder & Luogu

\(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

  • AtCoder & Luogu

若可以找到連續的兩段長度 \(\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

  • AtCoder & Luogu

對圖建一個 \(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

  • AtCoder & Luogu

先將 \(\{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

  • AtCoder & Luogu

數數好題。

考慮最後的顏色序列,一共有 \(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

  • AtCoder & Luogu

若兩個相反方向都同時出現或者同時不出現,則可以到達,否則不行。

點選檢視程式碼
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

  • AtCoder & Luogu

貪心,先與自己匹配,再與自己 +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!

  • AtCoder & Luogu

操作 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

  • AtCoder & Luogu

數論分討好題。

首先可以把對答案沒有影響的立方因子全部篩掉。

剩下的數都可以用 \(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

  • AtCoder & Luogu

如果一個 \(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)\)

點選檢視程式碼
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;
}

相關文章