2024牛客暑期多校训练营2

Preface

最下班的一集,100min 的时候手速过了六题,本来以为是完美开场,没想到是坐牢的开始

J 题很快推出了一个 \(O(n)\) 计算的组合式子,然后扔给徐神找生成函数做法,中间给出了几个要写快速阶乘算法的假做法后发现这题不太可做

祁神开始转战 D 题后给了个基于纳什均衡的很对的 DP 做法,然后二分的转移写了半天过不了样例,最后换成枚举后正确性是有保证了,但交上去又 TLE 了

过的人比较多的 G 题因为基本没怎么话时间想因此没意识到是个丁真题,最后徐神发现了 \(\sqrt {1000}\) 以内的质数只有 \(11\) 个,但已经于事无补了

最后靠着前期优秀的罚时苟了个尚可的排名,但后 200min 不出题让人感觉梦回去年啊


Floor Tiles

徐神看了眼就秒了,我题意都不知道,纯纯的被带飞

#include <bits/stdc++.h>

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) {
        int n, m, k; std::cin >> n >> m >> k;
        int x, y; std::string t; std::cin >> x >> y >> t;
        x--; y--;
        
        if(k < n + m) {
            std::cout << "No\n";
            continue;
        }
        
        std::vector<std::string> ans(n, std::string(m, 'A'));
        
        if((x + y & 1) ^ (t[0] == 'A'))
            for(int i = 0; i < n; ++i)
                for(int j = 0; j < m; ++j)
                    ans[i][j] = char('A' + (i + j & 1));
        else
            for(int i = 0; i < n; ++i)
                for(int j = 0; j < m; ++j)
                    ans[i][j] = char('B' - (i + j & 1));
        auto is_circle = [&](int i, int j) -> bool {
            return ans[i][j] == 'A' && ans[i][j + 1] == 'B' &&
               ans[i + 1][j] == 'B' && ans[i + 1][j + 1] == 'A';
        };
        int cir = 0, type = 0;
        for(int i = 0; i < n - 1; ++i) for(int j = 0; j < m - 1; ++j) if(is_circle(i, j)) {
            cir += 1;
            if(i == x && j == y) type = 1;
        }
        if(n + m + cir < k) {
            std::cout << "No\n";
            continue;
        }
        for(int i = 0; i < n - 1 && n + m + cir > k; ++i) for(int j = 0; j < m - 1 && n + m + cir > k; ++j) {
            if(is_circle(i, j)) {
                if(type) ans[i][j + 1] = 'A';
                else     ans[i][j] = 'B';
                cir -= 1;
            }
        }
        std::cout << "Yes" << char(10);
        for(int i = 0; i < n; ++i) std::cout << ans[i] << char(10);
    }
    return 0;
}

MST

感觉数据水了赛时随便写了个带 \(\log\) 的东西交上去一发过了,后面发现是有严格根号的做法的说

这种询问集合的题很容易想到根号分治,显然当 \(k_i\) 较大时直接 \(O(m\cdot\alpha(m))\) 跑一个克鲁斯卡尔即可

\(k_i\) 较小时理论上我们需要严格的 \(O(k_i^2)\) 做法,很容易想到用 Prim,但问题在于我们无法快速找出这些点之间的边

一种解决方案是对边的存储也使用根号分治,将点按照度数分类

度数 \(>\sqrt m\) 的点数较少,可以用临界矩阵存储;否则剩下的度数 \(\le \sqrt m\) 的点对应的边数较少,可以用邻接表存储

当然比赛时我直接用 map 存两点之间的边,然后还是暴力跑克鲁斯卡尔,交上去也过了

#include<cstdio>
#include<iostream>
#include<map>
#include<vector>
#include<algorithm>
#define RI int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=100005,LIM=250;
struct edge
{
    int x,y,v;
    inline edge(CI X=0,CI Y=0,CI V=0)
    {
        x=X; y=Y; v=V;
    }
    friend inline bool operator < (const edge& A,const edge& B)
    {
        return A.v<B.v;
    }
}E[N]; int n,m,q,k,p[N],fa[N]; map <pi,int> rst;
inline int getfa(CI x)
{
    return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
namespace Case1
{
    inline void solve(void)
    {
        RI i,j; for (i=1;i<=k;++i) fa[p[i]]=p[i];
        vector <edge> edges;
        for (i=1;i<=k;++i) for (j=i+1;j<=k;++j)
        {
            int x=p[i],y=p[j]; if (x>y) swap(x,y);
            if (rst.count(pi(x,y))) edges.emplace_back(x,y,rst[pi(x,y)]);
        }
        sort(edges.begin(),edges.end());
        int cnt=0; long long ret=0;
        for (auto [x,y,v]:edges)
        {
            if (getfa(x)!=getfa(y))
            {
                fa[getfa(x)]=getfa(y);
                ++cnt; ret+=v;
            }
            if (cnt==k-1) break;
        }
        if (cnt!=k-1) puts("-1"); else printf("%lld\n",ret);
    }
};
namespace Case2
{
    bool key[N];
    inline void solve(void)
    {
        RI i; for (i=1;i<=k;++i) fa[p[i]]=p[i],key[p[i]]=1;
        int cnt=0; long long ret=0;
        for (i=1;i<=m;++i)
        {
            int x=E[i].x,y=E[i].y;
            if (!key[x]||!key[y]) continue;
            if (getfa(x)!=getfa(y))
            {
                fa[getfa(x)]=getfa(y);
                ++cnt; ret+=E[i].v;
            }
            if (cnt==k-1) break;
        }
        for (i=1;i<=k;++i) key[p[i]]=0;
        if (cnt!=k-1) puts("-1"); else printf("%lld\n",ret);
    }
}
int main()
{
    RI i; for (scanf("%d%d%d",&n,&m,&q),i=1;i<=m;++i)
    {
        scanf("%d%d%d",&E[i].x,&E[i].y,&E[i].v);
        if (E[i].x>E[i].y) swap(E[i].x,E[i].y);
    }
    for (sort(E+1,E+m+1),i=1;i<=m;++i)
    if (!rst.count(pi(E[i].x,E[i].y))) rst[pi(E[i].x,E[i].y)]=E[i].v;
    while (q--)
    {
        for (scanf("%d",&k),i=1;i<=k;++i) scanf("%d",&p[i]);
        if (k<=LIM) Case1::solve(); else Case2::solve();
    }
    return 0;
}

Red Walking on Grid

首先不难发现假设起点在一个连通块的最左边,则存在一种最优的方案使得全程不会往左走

那么在第一行的点只有向下和向右两种操作了;在第二行的点就只能向上或向下

考虑 DP,令 \(f_{i,j,0/1}\) 表示当前位于 \((i,j)\) 位置,与其同列的另一个格子是否被经过的最长长度,转移十分显然

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5;
int n,f[2][N][2]; char a[2][N];
int main()
{
    RI i,j; for (scanf("%d",&n),i=0;i<2;++i) scanf("%s",a[i]+1);
    for (j=1;j<=n;++j)
    {
        for (i=0;i<2;++i) if (a[i][j]=='R') f[i][j][0]=max(f[i][j][0],1);
        for (i=0;i<2;++i) if (a[i][j]=='R')
        {
            if (a[i^1][j]=='R') f[i^1][j][1]=max(f[i^1][j][1],f[i][j][0]+1);
        }
        for (i=0;i<2;++i) if (a[i][j]=='R')
        {
            if (j+1<=n&&a[i][j+1]=='R') f[i][j+1][0]=max(f[i][j+1][0],max(f[i][j][0],f[i][j][1])+1);
        }
    }
    int ans=0; for (i=0;i<2;++i) for (j=1;j<=n;++j) ans=max(ans,max(f[i][j][0],f[i][j][1])-1);
    return printf("%d",ans),0;
}

Taking Candies

标算感觉十分奇怪,这里讲下祁神赛时写的做法,正确性是有保证的,但赛时 \(O(n\times x^2)\) TLE 了

首先很容易发现两人每次拿糖果都是拿当前最多的一袋,而关注最后一轮博弈过程,显然是谁钱多谁就拿走最后一袋

考虑从后往前递推,不难发现这是个纳什博弈模型,令 \(f_{i,x}\) 表示还剩下 \(i\) 袋糖果,先手此时手中有 \(x\) 块钱,在剩下的过程中最多能得到的糖果数

考虑枚举先手这一轮使用的筹码数 \(M\),若后手选择阻止先手(需要至少 \(M+1\) 的钱)会导致后续给先手带来的收益更大,则后手显然不会操作,则有转移:

\[f_{i,x}\leftarrow f_{i-1,x-M}+a_i\ (f_{i-1,x-M}+a_i<f_{i-1,x+M+1}) \]

否则分情况讨论一些特殊情况即可,总复杂度 \(O(n\times x^2)\),赛时最后挣扎了一波搞了个记搜剪了点状态,发现还是过不了

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

const int N = 1e5+5;
int n, xx, yy, tot, A[N];
int f[N][205];

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n >> xx >> yy;
    tot = xx+yy;
    for (int i=1; i<=n; ++i) cin >> A[i];
    sort(A+1, A+n+1);
    
    for (int x=(tot+1)/2; x<=tot; ++x) f[1][x]=A[1];
    for (int i=2; i<=n; ++i){
        for (int x=0; x<=tot; ++x){
            int L=0, R=min(x, tot-x);  
            for (int M=L; M<=R; ++M){
                if (f[i-1][x-M]+A[i] < f[i-1][x+M+1]){
                    f[i][x] = max(f[i][x], f[i-1][x-M]+A[i]);
                }else{
                    if (M >= tot-x) f[i][x] = max(f[i][x], f[i-1][x-M]+A[i]);
                    else f[i][x] = max(f[i][x], f[i-1][x+M+1]);
                }
            }
        }
    }
    cout << f[n][xx] << '\n';
    return 0;
}

Upt:祁神赛后用了种神秘的两次二分把这题 \(O(n\times x\log (x))\) 淦过去了,主要是这个 \(f_{i,\cdot}\) 它是阶梯上升的,因此要找关键点会比较麻烦

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

const int N = 1e5+5;
const int INF = 1e18+5;
int n, xx, yy, tot, A[N];
int f[N][205];

signed main(){
//	freopen("1.in", "r", stdin);
//	freopen("1.out", "w", stdout);
	ios::sync_with_stdio(0); cin.tie(0);
	cin >> n >> xx >> yy;
	tot = xx+yy;
	for (int i=1; i<=n; ++i) cin >> A[i];
	sort(A+1, A+n+1);
	
	for (int i=1; i<=n; ++i) f[i][tot+1] = INF;
	for (int x=(tot+1)/2; x<=tot; ++x) f[1][x]=A[1];
	for (int i=2; i<=n; ++i){
		for (int x=0; x<=tot; ++x){
			int y = tot-x;
			
			int bd1=-1, bd2=-1;
			
			int L=0, R=min(x, y);
			while (L<R){
				int M = L+(R-L)/2+1;
				if (f[i-1][x-M]+A[i] > f[i-1][x+M+1]) L=M;
				else R=M-1;
			}
			bd2=L;
			
			L=0, R=min(x, y);
			while (L<R){
	            int M = L+(R-L)/2;
	            if (f[i-1][x-M]+A[i] < f[i-1][x+M+1]) R=M;
	            else L=M+1;
	        }
			bd1=L;
			
			if (x+bd2+1<=tot) f[i][x] = max(f[i][x], f[i-1][x+bd2+1]);
			else f[i][x] = max(f[i][x], f[i-1][x-bd2]+A[i]);
			
			if (bd1>bd2){
				f[i][x] = max(f[i][x], f[i-1][x-bd1]+A[i]);
			}
			if (bd1>bd2+1){
				f[i][x] = max(f[i][x], min(f[i-1][x+(bd2+1)+1], f[i-1][x-(bd1-1)]+A[i]));
			}
	cout << f[n][xx] << '\n';
	return 0;
}

GCD VS XOR

徐神开场写的,还抢到了二血,而我题意都不知道

#include <bits/stdc++.h>

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) {
        int64_t x; std::cin >> x;
        int64_t l = x&-x;
        if(l == x) std::cout << "-1\n";
        else       std::cout << (x ^ l) << char(10);
    }
    return 0;
}

Mixed Juice

这场的防 AK,直接弃疗


The Set of Squares

唐完了,早知道就不该把 J 题扔给徐神,不然徐神早一小时看这题可能都出了

需要发现 \(<\sqrt {1000}=32\) 的质数只有 \(11\) 个,这也就意味着每个数最多含有一个 \(\ge 32\) 的质数

因此这些较大的质数只能相互匹配,所以可以分开来处理

而较小的质数由于数量很小,并且我们只关心每个质数此时的次数是奇数还是偶数,因此可以状压

用一个类似背包的 DP 即可做到 \(O(2^{11}\times n)\),转移十分显然

#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=1<<11,pri[11]={2,3,5,7,11,13,17,19,23,29,31},mod=1e9+7;
int n,x,f[2][N][2]; vector <pi> vec[N];
inline void inc(int& x,CI y)
{
    if ((x+=y)>=mod) x-=mod;
}
int main()
{
    RI i,j; for (scanf("%d",&n),i=1;i<=n;++i)
    {
        scanf("%d",&x); int mask=0,val=1;
        for (j=0;j<11;++j)
        {
            while (x%pri[j]==0)
            {
                if ((mask>>j)&1) val=1LL*val*pri[j]%mod;
                x/=pri[j]; mask^=(1<<j);
            }
        }
        vec[x].push_back(pi(mask,val));
    }
    int now=0; f[0][0][0]=1;
    auto calc=[&](CI x,CI y)
    {
        int val=1; for (RI i=0;i<11;++i)
        if (((x&y)>>i)&1) val=1LL*val*pri[i]%mod;
        return val;
    };
    for (auto [mask,val]:vec[1])
    {
        int nxt=now^1;
        for (i=0;i<N;++i) f[nxt][i][0]=f[now][i][0];
        for (i=0;i<N;++i) inc(f[nxt][i^mask][0],1LL*f[now][i][0]*val%mod*calc(mask,i)%mod);
        now=nxt;
    }
    for (j=32;j<=1000;++j) if (!vec[j].empty())
    {
        for (i=0;i<N;++i) f[now][i][1]=0;
        for (auto [mask,val]:vec[j])
        {
            int nxt=now^1;
            for (i=0;i<N;++i) f[nxt][i][0]=f[now][i][0],f[nxt][i][1]=f[now][i][1];
            for (i=0;i<N;++i)
            {
                inc(f[nxt][i^mask][0],1LL*f[now][i][1]*val%mod*calc(mask,i)%mod*j%mod);
                inc(f[nxt][i^mask][1],1LL*f[now][i][0]*val%mod*calc(mask,i)%mod);
            }
            now=nxt;
        }
    }
    return printf("%d",(f[now][0][0]-1+mod)%mod),0;
}

Instructions Substring

不难发现枚举左端点 \(l\),假设其对应的第一个走到 \((x,y)\) 的位置为 \(r\),则贡献为 \(n-r+1\)

判断是否走到的充要条件是 \(X_r-X_{l-1}=x\and Y_r-Y_{l-1}=y\),其中 \(X,Y\) 分别为两维坐标的前缀和数组

倒着用 map 维护一下最靠左的 \((X_i,Y_i)\) 即可

#include<cstdio>
#include<iostream>
#include<map>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=200005;
int n,x,y,px[N],py[N]; map <pi,int> fst; char a[N];
int main()
{
    RI i; scanf("%d%d%d%s",&n,&x,&y,a+1);
    if (x==0&&y==0) return printf("%lld",1LL*n*(n+1)/2LL),0;
    for (i=1;i<=n;++i)
    {
        px[i]=px[i-1]; py[i]=py[i-1];
        if (a[i]=='A') --px[i]; else
        if (a[i]=='D') ++px[i]; else
        if (a[i]=='W') ++py[i]; else
        if (a[i]=='S') --py[i];
    }
    //for (i=1;i<=n;++i) printf("%d %d\n",px[i],py[i]);
    long long ans=0; for (i=n;i>=1;--i)
    {
        fst[pi(px[i],py[i])]=i;
        pi tmp=pi(x+px[i-1],y+py[i-1]);
        if (fst.count(tmp)) ans+=n-fst[tmp]+1;
    }
    return printf("%lld",ans),0;
}

Red Playing Cards

看完题很容易想到一个暴力的 \(O(n^3)\) 区间 DP 做法,但题面显然要求的是 \(O(n^2)\)

考虑对于一个区间,在删除它之前可以先对其内部的一些较小的区间进行操作;而不在其内部的区间要么和它没有影响,要么会导致该区间无法操作

因此把所有区间按照长度从小到大处理,每次枚举其内部从哪些区间进行了操作,总复杂度 \(O(n^2)\)

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

const int N = 6005;
int n;
int A[N], pos[N][2], len[N], id[N];
int f[N][N];

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    pos[0][0]=0, pos[0][1]=2*n+1, len[0]=2*n+1;
    for (int i=1; i<=2*n; ++i){
        cin >> A[i];
        if (pos[A[i]][0]==0) pos[A[i]][0]=i;
        else pos[A[i]][1]=i, len[A[i]]=pos[A[i]][1]-pos[A[i]][0];
    }
    for (int i=0; i<=n; ++i) id[i]=i;
    sort(id, id+n+1, [&](int a, int b){return len[a]<len[b];});
    for (int dd=0; dd<=n; ++dd){
        int x=id[dd];
        int L=pos[x][0], R=pos[x][1];
        
        f[x][L]=x;
        for (int i=L+1; i<=R; ++i){
            f[x][i] = f[x][i-1]+x;
            if (pos[A[i]][1]==i && pos[A[i]][0]>=L){
                f[x][i] = max(f[x][i], f[x][pos[A[i]][0]-1] + f[A[i]][pos[A[i]][1]]);
            }
        }
        
//         printf("dd=%d f[%d]=%d\n", dd, x, f[x][R]);
    }
//     for (int i=0; i<=n*2+1; ++i) printf("%d ", f[0][i]); puts("");
    cout << f[0][2*n+1] << '\n';
    
    return 0;
}

Involutions

只会这题的组合意义式子,但 \(O(n)\) 的做法还是过不了,生成函数也没办法处理

考虑枚举不动点的个数 \(i\),则剩下的 \(2y=n-i\) 个点构成的置换环长度都必须恰好为 \(2\)

\(C_{2y}^y\) 表示选出 \(y\) 个点作为起点,\(y!\) 表示剩下的 \(y\) 个点关于这些点的匹配方案,但由于两边是对称的,因此要除以 \(2^y\)

最后的式子就是:

\[\sum_{y=0}^{2y\le n} \frac{a^{n-2y}\times C_{2y}^y\times y!}{2^y} \]

然而因为没推递推的关系式,所以根本想不到正解是整式递推(知道了也没用反正写不来),直接弃疗


Postscript

白天打牛客多校,晚上还有 CF 1+2,明天还有杭电多校,真是冲冲又实实啊

posted @ 2024-07-18 20:40  空気力学の詩  阅读(145)  评论(0编辑  收藏  举报