The 3rd Universal Cup. Stage 8: Cangqian

空気力学の詩發表於2024-10-03

Preface

戰術最失敗的一集,徐神從開場就被模擬題關住了,直到 4h30min 的時候才放出來

然後剩下的題也開的不順,最後感覺好多題都沒寫就 7 題下班了

這場也是延續了之前幾場的一貫畫風,最後 1h 我在狂暴鴻儒一個題,然後掛飛了也沒過,賽後稍微一想就發現又犯了個唐氏錯誤


A. Bingo

溝槽的大模擬

首先對於倍數的情況寫一個高精模低精和高精加高精即可,考慮子串的情況如何處理

手玩下會發現最後子串出現的位置不會偏移末尾太多,大約是 \(m\) 的長度級別,然後模擬下這一部分即可

實現的時候要注意各種神秘細節

#include <bits/stdc++.h>

struct bignum {
    int a[1000005], w;
    bignum& assign(const int rhs) {
        w = 0;
        return (*this) += rhs;
    }
    bignum& assign(const std::string &s) {
        if(s == "0") { w = 0; return *this; }
        w = s.size();
        for(int i = 0; i < w; ++i) a[i] = s[w - 1 - i] - '0';
        return *this;
    }
    bignum& operator +=(const int rhs) {
        a[w] = 0, a[0] += rhs;
        // std::cout << "a[0] = " << a[0] << char(10);
        for(int i = 0; i < w; ++i) {
            a[i + 1] += a[i] / 10;
            a[i] %= 10;
        }
        for(int i = w; a[i]; ++i) {
            a[i + 1] = a[i] / 10;
            a[i] %= 10;
            w++;
        }
        return *this;
    }
    int operator %(const int m) const {
        int64_t res = 0;
        for(int i = w - 1; i >= 0; --i) res = (res * 10 + a[i]) % m;
        return res;
    }
};

std::ostream& operator << (std::ostream& out, const bignum &n) {
    if(n.w == 0) out << '0';
    else for(int i = n.w - 1; i >= 0; --i) out << char(n.a[i] + '0');
    return out;
}

template<typename I>
int64_t to_int(I __first, I __last) {
    int64_t res = 0;
    while(__first < __last) res = res * 10 + (*__first++ - '0');
    return res;
}

void work() {
    std::string sn, sm;
    std::cin >> sn >> sm;
    static bignum n;
    n.assign(sn);
    int64_t m = to_int(sm.begin(), sm.end());
    n += 1;
    int64_t ans = n % m;
    if(ans == 0) {
        std::cout << n << char(10);
        return ;
    }
    ans = m - ans;
    int64_t hkr = 0, b10 = 1;
    for(int i = 0; i <= n.w - int64_t(sm.size()); ++i) {
        int64_t part = 0;
        for(int j = sm.size() - 1; j >= 0; --j)
            part = part * 10 + n.a[i + j];
        // std::cerr << "part = " << part << char(10);
        if(part == m) { ans = 0; break; }
        if(part < m && i <= 15) {
            __int128_t upd = m - part;
            for(int k = 0; k < i; ++k) upd *= 10;
            for(int k = 0, b10 = 1; k < i; ++k, b10 *= 10)
                upd -= b10 * n.a[k];
            ans = std::min(__int128_t(ans), upd);
        }
        if(i > 0 && part == m - 1) /*std::cerr << "FUCK " << hkr << "\n", */ans = std::min(int64_t(ans), hkr);
        if(i == 0) hkr = 10 - n.a[i];
        else {
            hkr += b10 * (9 - n.a[i]);
            if(hkr > ans) hkr = ans;
        }
        b10 *= 10;
        if(b10 > ans) b10 = ans;
    }
    n += ans;
    std::cout << n << char(10);
    return ;
}

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) work();
    return 0;
}


B. Simulated Universe

直到比賽結束我才知道這個題的題意,後面發現是個很丁真的 DP 題

考慮設 \(f_{i,j}\) 表示處理了前 \(i\) 個位置,且留有 \(j\) 次向後的升級時,最多能升級的次數

轉移按照定義十分顯然,複雜度 \(O(n^2)\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=8005,INF=1e9;
int t,n,f[N][N],a,b; char ch[10];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=0;i<=n;++i) for (RI j=0;j<=n+1;++j) f[i][j]=-INF;
        f[0][0]=0; int pre=0;
        for (RI i=1;i<=n;++i)
        {
            scanf("%s",ch);
            if (ch[0]=='B')
            {
                ++pre;
                for (RI j=0;j<=n;++j)
                f[i][j]=max(f[i][j],max(f[i-1][j],f[i-1][j+1]+1));
            } else
            {
                scanf("%d%d",&a,&b);
                for (RI j=0;j<=n;++j)
                {
                    f[i][j]=max(f[i][j],min(pre,f[i-1][j]+a));
                    f[i][j]=max(f[i][j],f[i-1][max(0,j-b)]);
                }
            }
        }
        int ans=0; for (RI i=0;i<=n;++i) ans=max(ans,f[n][i]);
        printf("%d\n",pre+ans);
    }
    return 0;
}

C. Challenge NPC

神秘構造題,想到二分圖就很簡單了

構造一個左右各 \(k+2\) 個點的二分圖,每部分的點向另一側編號嚴格小於它的點連邊

此時按照題目中的貪心法得到的顏色數為 \(k+2\),二分圖的實際染色數為 \(2\),符合題意

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=1024;
int k,col[N],L[N],R[N]; vector <pair <int,int>> E;
int main()
{
    scanf("%d",&k);
    for (RI i=2;i<=k+2;++i) for (RI j=1;j<i;++j)
    E.push_back({2*i-1,2*j}),E.push_back({2*i,2*j-1});
    printf("%d %d %d\n",(k+2)*2,(int)E.size(),2);
    for (RI i=1;i<=k+2;++i) printf("%d %d ",1,2); putchar('\n');
    for (auto [x,y]:E) printf("%d %d\n",x,y);
    return 0;
}

D. Puzzle: Easy as Scrabble

神秘搜尋題

考慮把限制看作推箱子,往一個固定方向一直走直到遇到一個不是 x 的位置為止,然後將這個限制暫存

如果某個位置上有多種型別的箱子,則該位置只能是 x,同時將該位置放進一個佇列中,每次取出隊首並將上面暫存的限制繼續推動即可

由於每個位置只會被變成 x 一次,且變成 x 後至多隻有四個方向的箱子會經過它,因此複雜度是 \(O(nm)\)

#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int n,m; char s[N][N]; vector <pair <int,int>> res[N][N];
queue <pair <int,int>> Q;
inline bool GO(int x,int y,CI d,CI c)
{
    if (c=='.') return 1;
    for (;;x+=dx[d],y+=dy[d])
    {
        if (x<1||x>n||y<1||y>m) return 0;
        if (s[x][y]!='x')
        {
            res[x][y].push_back({d,c});
            if (s[x][y]=='.') s[x][y]=c;
            else if (s[x][y]!=c)
            s[x][y]='x',Q.push({x,y});
            return 1;
        }
    }
    return 0;
}
int main()
{
    //freopen("D.in","r",stdin);
    scanf("%d%d",&n,&m);
    for (RI i=0;i<=n+1;++i) scanf("%s",s[i]);
    for (RI j=1;j<=m;++j)
    {
        if (!GO(1,j,1,s[0][j])) return puts("NO"),0;
        if (!GO(n,j,3,s[n+1][j])) return puts("NO"),0;
    }
    for (RI i=1;i<=n;++i)
    {
        if (!GO(i,1,0,s[i][0])) return puts("NO"),0;
        if (!GO(i,m,2,s[i][m+1])) return puts("NO"),0;
    }
    while (!Q.empty())
    {
        auto [x,y]=Q.front(); Q.pop();
        for (auto [d,c]:res[x][y])
        if (!GO(x,y,d,c)) return puts("NO"),0;
    }
    puts("YES");
    for (RI i=1;i<=n;++i,putchar('\n'))
    for (RI j=1;j<=m;++j)
    {
        if (s[i][j]=='x') s[i][j]='.';
        putchar(s[i][j]);
    }
    return 0;
}

E. Team Arrangement

\(n\le 60\) 就提示我們暴力求出分拆數,打表發現 \(60\) 的分法大概是 \(10^6\) 級別,可以接受

考慮驗證一組拆分是否合法,naive 的想法就是直接跑匹配,然後賽時寫了匈牙利和 Dinic 都沒衝過去

後面意識到要用性質,考慮從小到大列舉每個隊伍的大小 \(k\),對於所有 \(l_i\le k\) 且未被匹配的人,選擇 \(r_i\) 最小的 \(k\) 個與之匹配即可

用優先佇列實現可以卡過,看題解好像有用位運算做到去 $\log $ 的做法,感覺十分神秘

#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#define RI register int
#define CI const int&
using namespace std;
const int N=65,INF=1e9;
int n,cnt,l[N],r[N],w[N],ans=-INF; vector <int> vec,rpos[N];
inline int calc(vector <int>& vec)
{
    ++cnt;
    //for (auto x:vec) printf("%d ",x); putchar('\n');
    int res=0; for (auto x:vec) res+=w[x];
    if (res<ans) return -INF;
    priority_queue <int,vector <int>,greater <int>> hp;
    int k=1;
    for (auto x:vec)
    {
        while (k<=x)
        {
            for (auto y:rpos[k]) hp.push(y);
            ++k;
        }
        while (!hp.empty()&&hp.top()<x) hp.pop();
        if ((int)hp.size()<x) return -INF;
        for (RI i=1;i<=x;++i) hp.pop();
    }
    return res;
}
inline void DFS(vector <int>& vec,CI sum,CI lst)
{
    if (sum==n) return (void)(ans=max(ans,calc(vec)));
    for (RI i=lst;i<=n-sum;++i) vec.push_back(i),DFS(vec,sum+i,i),vec.pop_back();
}
int main()
{
    scanf("%d",&n);
    for (RI i=1;i<=n;++i) scanf("%d%d",&l[i],&r[i]),rpos[l[i]].push_back(r[i]);
    for (RI i=1;i<=n;++i) scanf("%d",&w[i]);
    DFS(vec,0,1);
    //printf("count  = %d\n",cnt);
    if (ans==-INF) puts("impossible"); else printf("%d",ans);
    return 0;
}

F. Stage: Agausscrab

簽到模擬題

#include<cstdio>
#include<iostream>
#include<string>
#include<algorithm>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
const int N=1005;
int n; pair <string,pair <int,int>> p[N];
int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    cin>>n;
    for (RI i=1;i<=n;++i) cin>>p[i].fi>>p[i].se.fi,p[i].se.se=i;
    auto cmp_rk=[&](const pair <string,pair <int,int>>& A,pair <string,pair <int,int>>& B)
    {
        return A.se.fi>B.se.fi;
    };
    sort(p+1,p+n+1,cmp_rk); int rk=1;
    for (RI i=1;i<=n;++i)
    {
        while (rk<i&&p[rk].se.fi>p[i].se.fi) ++rk;
        int tmp=min((int)p[i].fi.size(),rk);
        for (RI j=1;j<=tmp;++j) p[i].fi.pop_back();
    }
    auto cmp_id=[&](const pair <string,pair <int,int>>& A,pair <string,pair <int,int>>& B)
    {
        return A.se.se<B.se.se;
    };
    sort(p+1,p+n+1,cmp_id);
    string ans="";
    for (RI i=1;i<=n;++i) ans+=p[i].fi;
    if (!ans.empty()) ans[0]=ans[0]-'a'+'A';
    cout<<"Stage: "<<ans;
    return 0;
}

H. Permutation

關鍵點基本都想到了,結果最後犯病了分界點求的偏小了一直掛到比賽結束

考慮一個 naive 的二分做法,假設答案在 \([l,r]\) 內,我們先求出次大值所在的位置 \(smx\)

\(smx\in[l,mid]\),則我們詢問 \([l,mid]\),若返回值仍為 \(smx\) 則說明 \(n\) 一定在 \([l,mid]\) 中,此時可以繼續利用當前詢問的值,下次就不用先問整個區間的次大值了,否則詢問 \([mid+1,r]\),要多一次詢問;\(smx\in[mid+1,r]\) 的情況同理

但這樣最壞情況下詢問次數會達到 \(2\log_2 n\) 級別,因此要考慮最佳化,而題目中給出的 \(1.5\log_2 n\) 的界感覺就是要用不均勻分割

考慮令 \(f(x)\) 表示長度為 \(x\) 的區間至少要問多少次,不難根據上述做法寫出轉移式:

\[f(x)=\min_\limits{1\le y\le x-1} \max(f(y)+1,f(x-y)+2) \]

寫個暴力轉移 check 一下會發現這個東西求出的值恰好被上界 bound 住,但樸素的 DP 轉移是 \(O(n^2)\)

考慮快速求出最優轉移點,由於 \(\max\) 兩部分都是單調的,因此最優解一定在 tradeoff 的時候取得

不妨設最優轉移點對應的 \(\frac{y}{x}=\theta\),直接把 \(f(x)=1.5\log_2 x\) 帶進 \(f(\theta\cdot x)+1=f((1-\theta)\cdot x)+2\) 中解得 \(\theta = \frac{2^{\frac{2}{3}}}{1+2^{\frac{2}{3}}}\),這樣我們每次 DP 的時候在這附近取轉移點即可

最後實測可以卡著上界透過,據說正解好像是用黃金分割比來推的說

#include<cstdio>
#include<iostream>
#include<cmath>
#include<map>
#include<cassert>
#define RI register int
#define CI const int&
using namespace std;
const int N=1000000;
const double alpha=pow(2.0,2.0/3.0);
const double theta=alpha/(1.0+alpha);
int t,n,cnt,sum,f[N+5],pos[N+5]; map <pair <int,int>,int> rst;
inline int ask(CI l,CI r)
{
    assert(r-l+1>=2);
    if (rst.count({l,r})) return rst[{l,r}];
    printf("? %d %d\n",l,r); fflush(stdout);
    ++cnt; sum+=r-l+1;
    int res; scanf("%d",&res); return rst[{l,r}]=res;
}
inline void answer(CI p)
{
    printf("! %d\n",p); fflush(stdout);
}
int main()
{
    f[1]=0; f[2]=1;
    for (RI i=3;i<=N;++i)
    {
        int st=max(1,(int)(theta*i)-1);
        pos[i]=st; f[i]=max(f[st],f[i-st]+1)+1;
        for (RI j=1;j<=3;++j)
        {
            int y=min(st+j,i-1);
            int tmp=max(f[y],f[i-y]+1)+1;
            if (tmp<=f[i]) f[i]=tmp,pos[i]=y;
        }
    }
    //for (RI i=1;i<=N;++i) assert(f[i]<=(int)ceil(1.5L*log2(i)));
    // for (RI i=3;i<=20;++i) printf("%d %d\n",i,pos[i]);
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n); rst.clear(); cnt=0; sum=0;
        int l=1,r=n;
        while (r-l+1>2)
        {
            int smx=ask(l,r),len=pos[r-l+1];
            if (smx<=l+len-1)
            {
                int tmp=ask(l,l+len-1);
                if (smx==tmp) r=l+len-1; else l+=len;
            } else
            {
                int tmp=ask(r-len+1,r);
                if (smx==tmp) l=r-len+1; else r-=len;
            }
        }
        assert(l<=r);
        if (l==r) answer(l); else
        {
            int tmp=ask(l,r);
            if (tmp==l) answer(r); else answer(l);
        }
        assert(cnt<=(int)ceil(1.5L*log2(n)));
        assert(sum<=3*n);
    }
    return 0;
}

I. Piggy Sort

看題解好像不難的一個題,大致思路就是用抽屜原理發現列舉前兩張照片中的點對確定一條直線後可以直接確定一隻豬,後面交給祁神去補了


J. Even or Odd Spanning Tree

首先不難發現求出一個 MST 後其中一個答案就確定了

根據單峰性,很容易發現替換一條邊是最優的,即我們列舉一條不在生成樹上的邊 \((u,v,w)\),在生成樹上找到一條權值奇偶性與 \(w\) 不同,且權值最大的邊替換,倍增一下即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#include<array>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=500005,INF=1e18;
int t,n,m,fa[N],dep[N],anc[N][20],mx[N][20][2];
array <int,3> E[N]; vector <pair <int,int>> T[N];
inline int getfa(CI x)
{
    return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
inline void DFS(CI now=1,CI fa=0)
{
    dep[now]=dep[fa]+1; anc[now][0]=fa;
    for (RI i=0;i<19;++i)
    if (anc[now][i])
    {
        anc[now][i+1]=anc[anc[now][i]][i];
        for (RI j=0;j<2;++j) mx[now][i+1][j]=max(mx[now][i][j],mx[anc[now][i]][i][j]);
    } else break;
    for (auto [to,w]:T[now]) if (to!=fa)
    {
        mx[to][0][w&1]=w; mx[to][0][(w&1)^1]=-INF;
        DFS(to,now);
    }
}
inline int query(int x,int y,CI tp,int ret=-INF)
{
    if (dep[x]<dep[y]) swap(x,y);
    for (RI i=19;i>=0;--i) if (dep[anc[x][i]]>=dep[y])
    ret=max(ret,mx[x][i][tp]),x=anc[x][i];
    if (x==y) return ret;
    for (RI i=19;i>=0;--i) if (anc[x][i]!=anc[y][i])
    ret=max(ret,max(mx[x][i][tp],mx[y][i][tp])),x=anc[x][i],y=anc[y][i];
    return max(ret,max(mx[x][0][tp],mx[y][0][tp]));
}
signed main()
{
    for (scanf("%lld",&t);t;--t)
    {
        scanf("%lld%lld",&n,&m);
        for (RI i=0;i<=n;++i) for (RI j=0;j<20;++j) anc[i][j]=0;
        for (RI i=1;i<=m;++i) scanf("%lld%lld%lld",&E[i][1],&E[i][2],&E[i][0]);
        sort(E+1,E+m+1); vector <array <int,3>> Rep;
        for (RI i=1;i<=n;++i) fa[i]=i,T[i].clear();
        int sum1=0,cnt=0;
        for (RI i=1;i<=m;++i)
        {
            auto [w,u,v]=E[i];
            if (getfa(u)==getfa(v))
            {
                Rep.push_back(E[i]); continue;
            }
            fa[getfa(u)]=getfa(v); sum1+=w; ++cnt;
            T[u].push_back({v,w}); T[v].push_back({u,w});
        }
        if (cnt!=n-1)
        {
            puts("-1 -1"); continue;
        }
        int sum2=INF; DFS();
        for (auto [w,u,v]:Rep)
        {
            int ww=query(u,v,(w&1)^1);
            if (ww!=-INF) sum2=min(sum2,sum1-ww+w);
        }
        if (sum1&1) swap(sum1,sum2);
        if (sum1==INF) sum1=-1;
        if (sum2==INF) sum2=-1;
        printf("%lld %lld\n",sum1,sum2);
    }
    return 0;
}

M. Triangles

算出初始時總三角形數後,容斥減去經過被刪除的邊的三角形即可,簡單推一推式子

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

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n, a, b; cin >> n >> a >> b;
    int tot = 0;
    for (int i=1; i<=n; ++i) {
        tot += (n-i+2)*(n-i+1)/2;
        if (n-i >= i) tot += (n-2*i+2)*(n-2*i+1)/2;
    }
    tot -= b*(a+1-b);
    int res = 0;
    for (int i=1; i<=min(n-a, a); ++i) {
        res += min(a, b+i-1) - max(b-i, 0LL) + 1 - i;
    }
    tot -= res;
    cout << tot << '\n';
    return 0;
}

Postscript

今天直接被大一小登薄紗了,看來是離入土不遠了

相關文章