2024牛客暑期多校訓練營5

空気力学の詩發表於2024-07-30

Preface

坐牢,爽!

前期經典屢次被簽到腐乳導致罰時爆炸,寫完四題後發現排名已經衝刺 200+ 了,再一看後面的題都過的很少

跟著榜看了一些題後感覺都不太可做,祁神和徐神一直在討論 J 但我一點不想寫大分類討論 Counting 遂開擺

擺到大概三點半的時候發現 G 題過的隊越來越多了,看了眼題意後感覺就是個推結論用 DS 維護的東西

把徐神搖過來後徐神玩了會就給了我個很簡單的結論,我翻了個 LCT 板子魔改了下交上去就過了,算是將小局逆轉了

賽後補了 K,J 題經典扔給隊友寫自己白蘭去了,下面就只寫過了的題的做法,其它題之後再說吧


大力分討題,首先判掉格子總數為奇數的無解情況;以及 \(1\times 2\) 這種一定有解的情況

手玩一下會發現此時兩個限制都加上就一定無解,同時第二個限制其實很強,只有 \(1\times 2k\) 的情況有解

考慮僅有第一個限制時,可以構造出以下兩種最小結構單元(數字相同的表示同一塊骨牌):

1134
2234
5578
6678
1156
2356
2344

因此我們就有了 \(2\times 2\) 的構造單元和 \(2\times 3\) 的構造單元,簡單討論後發現用這兩種單元可以構造除了 \(1\times 2k\) 外的任何情況

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,n,m,a,b;
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d%d%d%d",&n,&m,&a,&b);
        if (n%2==1&&m%2==1) { puts("No"); continue; }
        if (n>m) swap(n,m);
        if (n==1&&m==2) { puts("Yes"); continue; }
        if (a==1&&b==1) { puts("Yes"); continue; }
        if (a==0&&b==0) { puts("No"); continue; }
        if (a==0)
        {
            if (n==1) puts("No"); else puts("Yes");
        } else
        {
            if (n==1) puts("Yes"); else puts("No");
        }
    }
    return 0;
}

簽到題,祁神開場寫的,我題目都沒看

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

const int N = 1e5+5;
int n, A[N], B[N];

void solve(){
    cin >> n;
    for (int i=1; i<=n; ++i) cin >> A[i];
    for (int i=1; i<=n; ++i) cin >> B[i];
    int cnt1=0, cnt2=0;
    for (int i=1; i<=n; ++i){
        if (A[i]>B[i]) ++cnt1;
        else if (A[i]==B[i]) ++cnt2;
    }
    cout << cnt1 + (cnt2+1)/2 << '\n';
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
    int t; cin >> t; while (t--) solve();
    return 0;
}

徐神的觀察力,輕鬆找出結論秒了此題

考慮建一棵新樹,對於每條實鏈,將其縮為一個點,而鏈底的點可以代表整個點的操作;剩餘的虛邊連通新樹的點

然後問題轉化為要在新樹上給每個點附上一個排列的值,使得每個點都比其父親的權值小

透過樹形 DP 和歸納法,可以發現答案為 \(\frac{n!}{\prod size_i}\),其中 \(size_i\) 表示節點 \(i\) 在原樹上的子樹大小,\(i\) 為每條實鏈鏈頂的點

在維護答案時我們可以在所有虛邊的底端點處統計貢獻,用不換根的 LCT 可以輕鬆維護,複雜度 \(O(n\log n)\)

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,mod=998244353;
int n,q,x,sz[N],tag[N],fact[N],ifac[N],inv[N],ans; vector <int> v[N];
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
	RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
	for (ifac[n]=quick_pow(fact[n]),i=n-1;i>=0;--i) ifac[i]=1LL*ifac[i+1]*(i+1)%mod;
	for (inv[0]=i=1;i<=n;++i) inv[i]=1LL*ifac[i]*fact[i-1]%mod;
}
inline void DFS(CI now=1)
{
	sz[now]=1; for (auto to:v[now]) DFS(to),sz[now]+=sz[to];
}
inline void work(CI x)
{
	tag[x]^=1;
	if (tag[x]) ans=1LL*ans*sz[x]%mod; else ans=1LL*ans*inv[sz[x]]%mod;
}
class Link_Cut_Tree
{
    private:
        struct splay
        {
            int ch[2],fa;
        }node[N];
        #define lc(x) node[x].ch[0]
        #define rc(x) node[x].ch[1]
        #define fa(x) node[x].fa
        inline void connect(CI x,CI y,CI d)
        {
            node[fa(x)=y].ch[d]=x;
        }
        inline int identify(CI now)
        {
            return rc(fa(now))==now;
        }
        inline bool isroot(CI now)
        {
            return lc(fa(now))!=now&&rc(fa(now))!=now;
        }
        inline void rotate(CI now)
        {
            int x=fa(now),y=fa(x),d=identify(now); if (!isroot(x)) node[y].ch[identify(x)]=now;
            fa(now)=y; connect(node[now].ch[d^1],x,d); connect(x,now,d^1);
        }
        inline void splay(int now)
        {
            for (int t;!isroot(now);rotate(now))
            t=fa(now),!isroot(t)&&(rotate(identify(now)!=identify(t)?now:t),0);
        }
        inline int findroot(int now)
        {
            while (lc(now)) now=lc(now); return now;
        }
    public:
        inline void link(CI x,CI y)
        {
            fa(x)=y;
        }
        inline void access(int x)
        {
            for (int y=0,t;x;x=fa(y=x))
            {
                splay(x); if (rc(x)) t=findroot(rc(x)),work(t);
                if (rc(x)=y) t=findroot(rc(x)),work(t);
            }
        }
        #undef lc
        #undef rc
        #undef fa
}LCT;
int main()
{
	RI i; for (scanf("%d%d",&n,&q),i=2;i<=n;++i)
	scanf("%d",&x),LCT.link(i,x),v[x].push_back(i);
	for (DFS(),init(n),ans=fact[n],i=1;i<=n;++i) ans=1LL*ans*inv[sz[i]]%mod;
	while (q--) scanf("%d",&x),LCT.access(x),printf("%d\n",ans);
	return 0;
}

爆搜,啟動!

題目等價於找一條最長的鏈,需要滿足這條鏈對應點集的匯出子圖對應的邊只有鏈上的邊

考慮用爆搜處理,用二元組 \((x,mask)\) 表示當前位於點 \(x\),之前鏈上的點集為 \(mask\)

每次轉移列舉下一步走的點 \(y\),需要滿足 \(x,y\) 間有邊,且 \(mask\)\(y\) 之間沒邊

根據經典複雜度分析,發現最壞情況下複雜度為 \(O(3^n\times n)\),可以透過

(話說 THU 很喜歡出這種爆搜然後證明覆雜度正確的題,之前做某個 Regional 好像也遇到過)

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
typedef long long LL;
const int N=45;
int n,m,x,y,ans; LL g[N]; vector <int> v[N];
inline void DFS(CI now,const LL& pre=0,CI len=1)
{
    ans=max(ans,len);
    for (auto to:v[now]) if (((pre>>to)&1)==0&&(g[to]&pre)==0) DFS(to,pre|(1LL<<now),len+1);
}
int main()
{
    RI i; for (scanf("%d%d",&n,&m),i=1;i<=m;++i)
    {
        scanf("%d%d",&x,&y); --x; --y;
        v[x].push_back(y); v[y].push_back(x);
        g[x]|=(1LL<<y); g[y]|=(1LL<<x);
    }
    for (i=0;i<n;++i) DFS(i);
    return printf("%d",ans),0;
}

祁神正在絕贊補題中,我直接開擺了


神秘 DP 題,完全沒想到這樣設狀態

\(f_{l,r,c}\) 表示已知答案在 \(a_l\sim a_r\) 之間,且在 \(l\) 左側的詢問次數減去在 \(r\) 右側的詢問次數為 \(c\) 的最小代價

之所以這麼設狀態是因為我們考慮在最後得知詢問結果時得到 \(x\) 對應的係數,在計算貢獻時左右各一次詢問會相互抵消,因此只要知道差值就能計算貢獻了

轉移的時候考慮列舉 \(y\in(a_k,a_{k+1}]\),最優的轉移點顯然就是讓貢獻儘量平均的點

但直接這麼做複雜度是 \(O(n^3\times |c|)\) 的,透過觀察可以發現 \(f_{l,r,c}\) 的決策點一定在 \(f_{l,r-1,c}\) 的決策點的右側,因此可以用 two pointers 最佳化

同時根據題解中的分析,\(c\) 的值小於 \(\log n\)級別,因此最後總複雜度為 \(O(n^2\log n)\)

#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,INF=1e18;
int n,a[N],f[N][N][85];
signed main()
{
    RI i,j,k; for (scanf("%lld",&n),i=1;i<=n;++i) scanf("%lld",&a[i]);
    for (i=1;i<=n;++i) for (j=1;j<=n;++j) for (k=0;k<=80;++k) f[i][j][k]=INF;
    for (i=1;i<=n;++i) for (j=-40;j<=40;++j) f[i][i][j+40]=a[i]*j;
    for (RI l=n;l>=1;--l) for (k=1;k<80;++k)
    {
        int p=l+1; for (RI r=l+1;r<=n;++r)
        {
            while (p<=r)
            {
                int L=f[l][p-1][k-1],R=f[p][r][k+1];
                if (L==INF||R==INF) break;
                int val=(R-L+1)/2;
                if (val>a[p]) val=a[p];
                if (val<=a[p-1]) val=a[p-1]+1;
                int tmp=max(L+val,R-val);
                if (tmp<f[l][r][k]) f[l][r][k]=tmp,++p; else break;
            }
            --p;
        }
    }
    return printf("%lld",f[1][n][40]),0;
}

由於貢獻只能往前移動不能往後,因此考慮從前往後把數一個個加進去

設當前加入了 \(a_i\)\(a_1\sim a_{i-1}\) 中的最小值為 \(M\),顯然若 \(a_i>M\) 則從 \(a_i\) 換一個 \(1\)\(M\) 一定不會讓答案變劣

暴力重複以上過程,複雜度 \(O(n^2\times a_i)\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,mod=998244353;
int t,n,a[N];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
        for (i=2;i<=n;++i)
        {
            for (;;)
            {
                int mn=101,pos=-1;
                for (j=1;j<i;++j) if (a[j]<mn) mn=a[j],pos=j;
                if (a[i]<=mn) break;
                ++a[pos]; --a[i];
            }
        }
        int ans=1; for (i=1;i<=n;++i) ans=1LL*ans*a[i]%mod;
        printf("%d\n",ans);
    }
    return 0;
}

Postscript

每天打多校感覺都在坐牢,感覺心態要被打出問題了苦露西

相關文章