The 2021 CCPC Weihai Onsite

发布时间 2023-10-15 16:52:23作者: 空気力学の詩

Preface

又被打爆了,看了下榜这场罚时比较炸喜提银首咯

不过yysy这场题出的还是挺好的,medium题都挺有意思需要想一想

但就是感觉考的组合计数这一块有点太多了,而且因为有人歪榜开局过了M,导致我前期一直在这道题上坐牢,最后还是徐神出马一套生成函数秒了此题


A. Goodbye, Ziyin!

签到题,如果有度数\(>3\)的点就一定不合法,否则所有度数\(<3\)的点都可以为根

然而这题直接scanf会TLE,不得不去copy个读优

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
#define Tp template <typename T>
using namespace std;
const int N=1e6+5;
int n,x,y,deg[N];
class FileInputOutput
{
    private:
        static const int S=1<<21;
        #define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
        #define pc(ch) (Ftop!=Fend?*Ftop++=ch:(fwrite(Fout,1,S,stdout),*(Ftop=Fout)++=ch))
        char Fin[S],Fout[S],*A,*B,*Ftop,*Fend; int pt[30];
    public:
        inline FileInputOutput(void) { Ftop=Fout; Fend=Fout+S; }
        Tp inline void read(T& x)
        {
            x=0; char ch; while (!isdigit(ch=tc()));
            while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
        }
        Tp inline void write(T x)
        {
            RI ptop=0; while (pt[++ptop]=x%10,x/=10);
            while (ptop) pc(pt[ptop--]+48); pc('\n');
        }
        inline void flush(void)
        {
            fwrite(Fout,1,Ftop-Fout,stdout);
        }
        #undef tc
        #undef pc
}F;
int main()
{
	//freopen("A.in","r",stdin); freopen("A.out","w",stdout);
	RI i; for (F.read(n),i=1;i<n;++i)
	F.read(x),F.read(y),++deg[x],++deg[y];
	int ans=0; for (i=1;i<=n;++i)
	{
		if (deg[i]>3) { ans=0; break; }
		if (deg[i]<3) ++ans;
	}
	return printf("%d",ans),0;
}

B. Subset

妈的怎么这么多数学题


C. Assign or Multiply

看到这题秒出了一个原根转换后再bitset做法,但感觉这数据范围铁跑不过就没写了


D. Period

比较Easy的字符串,被徐神一眼秒了,我题目都没看过

#include <bits/stdc++.h>

char s[1000005], c, *p;
int fail[1000005];

inline int readi() {
	int res = 0;
	while(c > '9' || c < '0') c = getchar();
	while(c >= '0' && c <= '9')
		res = res * 10 + (c ^ 48),
		c = getchar();
	return res;
}

int main() {
	// freopen("1.in", "r", stdin);
	for(c = getchar(), p = s + 1; c != '\n' && c != '\r'; c = getchar()) *p++ = c;
	int n = p - s - 1, q;
	fail[0] = fail[1] = 0;
	for(int i = 2, j = 0; i <= n; ++i) {
		while(j && s[j + 1] != s[i]) j = fail[j];
		if(s[j + 1] == s[i]) j += 1;
		fail[i] = j;
	}
	std::vector<int> prd{0};
	for(int i = n; i; i = fail[i]) prd.push_back(i);
	std::sort(prd.begin(), prd.end());
	// for(int i = 0; i < prd.size(); ++i) std::cout << prd[i] << char(i == prd.size() - 1 ? 10 : 32);
	q = readi();
	while(q--) {
		int p = readi();
		int s = std::min(p - 1, n - p);
		auto l = prd.begin(), r = prd.end();
		while(l < r) {
			auto mid = l + (r - l + 1) / 2;
			if(*mid > s) r = mid - 1;
			else         l = mid;
		}
		std::cout << l - prd.begin() << char(10);
	}
	return 0;
}

E. CHASE!

ORZ祁神秒了此题

考虑设\(E(i)\)表示还能重选\(0\)次时的期望得分,则初始时\(E(0)=\frac{\sum_{i=1}^n (n-1)\times a_i}{C_n^2}\)

考虑对于\(E(1)\)来说,如果选出的数对\(a_i+a_j\ge E(0)\)的话就不需要重选,否则可以考虑重选,类似的我们可以把\(E(i-1)\)作为求\(E(1)\)的基准

由于\(k\)很小所以可以暴力\(O(nk)\)DP处理,每次转移求数对的个数可以用two pointers求解

而有了这个思路后询问也很简单了,注意特判\(c=0\)的情形

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#define int long long
#define double long double
#define RI register int
#define CI const int&
using namespace std;
const double EPS=1e-4;
const int N=100005;
int n,k,m,ta[N],a[N],pfx[N],all,x,y,c; double E[105];
inline int dcmp(const double& x)
{
	if (fabs(x)<EPS) return 0; return x<0?-1:1;
}
signed main()
{
	//freopen("E.in","r",stdin); freopen("E.out","w",stdout);
	RI i,p,q; for (scanf("%lld%lld%lld",&n,&k,&m),i=1;i<=n;++i) scanf("%lld",&a[i]),ta[i]=a[i];
	for (all=n*(n-1)/2LL,i=1;i<=n;++i) E[0]+=1.0L*(n-1)*a[i]; E[0]/=all;
	for (sort(a+1,a+n+1,greater<int>()),i=1;i<=n;++i) pfx[i]=pfx[i-1]+a[i];
	for (i=1;i<=k;++i)
	{
		int grt=0,sum=0; for (p=1,q=n;p<q;++p)
		{
			while (p<q&&a[p]+a[q]<E[i-1]) --q;
			if (p<q) grt+=q-p,sum+=(q-p)*a[p]+pfx[q]-pfx[p];
		}
		E[i]=(1.0L*(all-grt)*E[i-1]+sum)/all;
	}
	for (printf("%.10Lf\n",E[k]),i=1;i<=m;++i)
	{
		scanf("%lld%lld%lld",&x,&y,&c);
		if (c==0) { puts("accept"); continue; }
		int d=dcmp(ta[x]+ta[y]-E[c-1]);
		if (d<0) puts("reselect"); else
		if (d>0) puts("accept"); else puts("both");
	}
	return 0;
}

F. Stone

我上机写E的一点功夫这题就被祁神秒了,我直接ORZ

考虑如果对于当前的局面,存在某堆石子为奇数,那么先手只要在第一步把所有堆都拿成偶数,接下来就可以通过镜像操作获胜了

否则考虑所有堆都是偶数的情况,则所有人都不能取奇数,那么就等价于把所有数都除以\(2\)然后再判断上述情况即可

最后的答案就是\(\frac{1+\min a_i}{2}\),总复杂度\(O(n\times \log a_i)\)

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

const int N = 1e6+5;
int n, A[N];
signed main(){
	scanf("%lld", &n);
	for (int i=1; i<=n; ++i) scanf("%lld", &A[i]);
	bool ok=false;
	do{
		for (int i=1; i<=n; ++i){
			if (A[i]%2){
				ok=true;
				break;
			}
		}
		if (!ok) for (int i=1; i<=n; ++i) A[i]/=2;
	}while (!ok);
	
	int ma=1e9+9;
	for (int i=1; i<=n; ++i) if (A[i]%2) ma=min(ma, A[i]);
	printf("%lld\n", (ma+1)/2);
	return 0;	
}

G. Shinyruo and KFC

首先不难发现对于固定的\(k\),答案其实就是\(\prod_{i=1}^n C_{k}^{a_i}\)

然后在想了各种奇奇怪怪的多项式科技后,终于顿悟了\(\sum_{i=1}^n a_i\le 10^5\)的限制的作用,就是保证最后不同的\(a_i\)只有\(\sqrt {10^5}\)种,因此可以直接暴力求解

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005,mod=998244353;
int n,m,a[N],fact[N],ifac[N],c[N],cnt;
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;
}
inline int C(CI n,CI m)
{
	if (n<0||m<0||n<m) return 0;
	return 1LL*fact[n]*ifac[m]%mod*ifac[n-m]%mod;
}
int main()
{
	//freopen("G.in","r",stdin); freopen("G.out","w",stdout);
	RI i,j; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%d",&a[i]),++c[a[i]];
	for (i=0;i<=100000;++i) if (c[i]) a[++cnt]=i;
	for (init(100000),i=1;i<=m;++i)
	{
		int ret=1; for (j=1;j<=cnt;++j)
		ret=1LL*ret*quick_pow(C(i,a[j]),c[a[j]])%mod;
		printf("%d\n",ret);
	}
	return 0;
}

H. city safety

怎么大家都会树上DP的做法,就我苦思冥想了半天才会网络流建图……

这里简单讲一下我的最小割做法吧,感觉和题解的好像还不太一样

首先假设每个点都取得最大答案\(v_{n-1}\),考虑减去尽量少的值来得到一种合法的方案

  • 对于原图中的\(i\in [1,n]\)点,连\(i\to t\),容量为\(w_i\)的边,割掉这条边表示将这个点选上
  • 新建\(n\times n\)个辅助点,第\(i\in[1,n]\)行,第\(j\in[0,n-1]\)列的点表示将与原图中与\(i\)距离等于\(j\)的所有点保留
  • \(s\)与每行的第一个点连边,容量为\(v_0\)
  • 将每行的第\(j\)个点与第\(j+1\)个点连边,容量为\(v_{n-1-v_j}\)
  • \((i,j)\)这个辅助点向原图中满足\(dis(i,k)=j\)的点\(k\)连边,边权为\(\infty\)

然后跑一个最小割用\(n\times v_{n-1}\)减去即可,具体合理性可以把图画出来手玩一下

这个做法其实并没有用到树的性质,而这题还有一个我看不太懂原理但很好写的树上DP做法,纯被吊打

#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=205,INF=1e12;
int n,w[N],v[N],dis[N][N],x,y,id[N][N],ID[N],idx;
namespace Network_Flow
{
	const int NN=100005,MM=1e7+5;
	struct edge
	{
		int to,nxt,v;
	}e[MM<<1]; int cnt=1,head[NN],cur[NN],dep[NN],s,t;
	inline void addedge(CI x,CI y,CI z)
	{
		//printf("%lld -> %lld (%lld)\n",x,y,z);
		e[++cnt]=(edge){y,head[x],z}; head[x]=cnt;
		e[++cnt]=(edge){x,head[y],0}; head[y]=cnt;
	}
	#define to e[i].to
	inline bool BFS(void)
	{
		memset(dep,0,(t+1)*sizeof(int)); dep[s]=1;
		queue <int> q; q.push(s);
		while (!q.empty())
		{
			int now=q.front(); q.pop();
			for (RI i=head[now];i;i=e[i].nxt)
			if (e[i].v&&!dep[to]) dep[to]=dep[now]+1,q.push(to);
		}
		return dep[t];
	}
	inline int DFS(CI now,CI tar,int dis)
	{
		if (now==tar) return dis; int ret=0;
		for (RI& i=cur[now];i&&dis;i=e[i].nxt)
		if (e[i].v&&dep[to]==dep[now]+1)
		{
			int dist=DFS(to,tar,min(dis,e[i].v));
			if (!dist) dep[to]=INF;
			dis-=dist; ret+=dist; e[i].v-=dist; e[i^1].v+=dist;
			if (!dis) return ret;
		}
		if (!ret) dep[now]=0; return ret;
	}
	#undef to
	inline int Dinic(int ret=0)
	{
		while (BFS()) memcpy(cur,head,(t+1)*sizeof(int)),ret+=DFS(s,t,INF); return ret;
	}
};
using namespace Network_Flow;
signed main()
{
	// freopen("H.in","r",stdin); freopen("H.out","w",stdout);
	RI i,j,k; for (scanf("%lld",&n),i=1;i<=n;++i) scanf("%lld",&w[i]);
	for (i=0;i<n;++i) scanf("%lld",&v[i]);
	for (i=1;i<=n;++i) for (j=1;j<=n;++j) dis[i][j]=i!=j?INF:0;
	for (i=1;i<n;++i) scanf("%lld%lld",&x,&y),dis[x][y]=dis[y][x]=1;
	for (k=1;k<=n;++k) for (i=1;i<=n;++i) for (j=1;j<=n;++j)
	dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
	for (s=idx=1,i=1;i<=n;++i) for (j=0;j<n;++j) id[i][j]=++idx;
	for (i=1;i<=n;++i) ID[i]=++idx; t=++idx;
	for (i=1;i<=n;++i)
	{
		addedge(s,id[i][0],v[n-1]); addedge(ID[i],t,w[i]);
		for (j=0;j+1<n;++j) addedge(id[i][j],id[i][j+1],v[n-1]-v[j]);
		for (j=0;j<n;++j) for (k=1;k<=n;++k)
		if (dis[i][k]==j) addedge(id[i][j],ID[k],INF);
	}
	return printf("%lld",n*v[n-1]-Dinic()),0;
}

I. Distance

这题好像之前暑假前集训做过一个类似的来着?不过好像本质也不太一样,弃了弃了

(妈的怎么都是数学题)


J. Circular Billiard Table

签到题,手玩一下会发现答案就是\(\frac{\operatorname{LCM(180b,a)}}{a}-1\)

#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
int t,a,b;
inline __int128 LCM(const __int128& x,const __int128& y)
{
	return x/__gcd(x,y)*y;
}
inline void print(const __int128& x)
{
	if (x>9) print(x/10); putchar(x%10+'0');
}
int main()
{
	//freopen("J.in","r",stdin); freopen("J.out","w",stdout);
	for (scanf("%d",&t);t;--t)
	scanf("%d%d",&a,&b),print(LCM(180LL*b,a)/a-1),putchar('\n');
	return 0;
}

K. Tiny Stars

神之构造题,不得不佩服出题人的脑洞

考虑要让连通块尽量小,而由于又是一个外向图很容易想到把每个连通块搞成一个环

而注意到\(i\to \frac{i}{a}\to \frac{a^2}{i}\to \frac{a}{i}\to i\)(分别执行\(0,1,0,1\)操作),因此只要能实现交替连边,就可以把图变成若干个四元环,代价就是\(4n\)

而这样连边时会均摊\(0/1\)的个数,因此连边的复杂度就是\(\frac{n}{2}\times (1+\lceil\log_2 n\rceil)\le 7.5n\),加上前面的\(4n\)恰好小于\(12n\)的限制

现在的问题就是怎么实现这个交替连边的过程了,要求使得每个\(i\)连向的点的状态都和自己的不同

一种很妙的方法是先给\(n\)求个原根\(g\),把乘除法转换为指数上的加减法,然后\(i\)的状态为\(0\)当且仅当\(i=g^x\)\(x\)为奇数

不难发现此时只要\(a\)的指数为奇数,那么这个过程就是对的,因此出错概率为\((\frac{1}{2})^{20}\),完全可以忽略

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=10005;
int n,g,ans[N];
inline int quick_pow(int x,int p,int mod,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline vector <int> resolve(int x)
{
	vector <int> frac;
	for (RI i=2;i*i<=x;++i) if (x%i==0)
	{
		frac.push_back(i);
		while (x%i==0) x/=i;
	}
	if (x>1) frac.push_back(x);
	return frac;
}
inline int get_root(CI p)
{
	vector <int> frac=resolve(p-1);
	for (RI i=2;i<p;++i)
	{
		bool flag=1; for (auto x:frac)
		if (quick_pow(i,(p-1)/x,p)==1) { flag=0; break; }
		if (flag) return i;
	}
}
int main()
{
	//freopen("K.in","r",stdin); freopen("K.out","w",stdout);
	RI i; for (scanf("%d",&n),i=1;i<=n;++i) ans[i]=1;
	for (g=get_root(n),i=1;i<n;i+=2) ans[quick_pow(g,i,n)]=0;
	for (i=1;i<n;++i) putchar(ans[i]+'0'),putchar(' ');
	return 0;
}

L. shake hands

题目都没看,经典防AK题


M. 810975

810975,新人怎么听不懂,没想到短短两年后炉石国服就寄了,默哀

这题我们队推不出容斥做法,因此只能靠徐神通过生成函数优化DP的式子,推出来最长段的长度小于等于\(k\)的方案数就是

\[[x^m](1+x+x^2+\cdots+x^k)^{n-m+1} \]

然后上去大力Rush了个多项式快速幂板子切了这道题,赛后一看妈的大家都会容斥

不过后面看了下参考的经典容斥问题是我18年打过的杭电多校来着,我直接呃呃

#include <bits/stdc++.h>

using llsi = long long signed int;

const int $n = 1 << 19;

const llsi mod = 998244353, G = 3, Gi = 332748118;

llsi ksm(llsi a, llsi b) {
	llsi res = 1;
	while(b) {
		if(b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

int tae[$n] {0};
void otae(int n) {
	for(int i = 1; i < n; ++i)
		tae[i] = tae[i >> 1] >> 1 | ((i & 1) ? n >> 1 : 0);
	return ;
}

int c2(int n) {
	int i = 1; while(i < n) i <<= 1;
	return i;
}

void NTT(llsi *a, int an, int on) {
	for(int i = 1; i < an; ++i) if(tae[i] > i) {
		llsi tmp = a[i]; a[i] = a[tae[i]];
		a[tae[i]] = tmp;
	}
	
	for(int i = 1; i < an; i <<= 1) {
		const llsi wi = ksm(on > 0 ? G : Gi, (mod - 1) / (i << 1));
		for(int j = 0; j < an; j += i << 1) {
			llsi w = 1;
			for(int k = 0; k < i; ++k) {
				llsi x = a[j + k], y = a[j + k + i] * w % mod;
				a[j + k] = (x + y) % mod;
				a[j + k + i] = (x + mod - y) % mod;
				w = w * wi % mod;
			}
		}
	}
	
	if(on < 0) {
		llsi inv = ksm(an, mod - 2);
		for(int i = 0; i < an; ++i) a[i] = a[i] * inv % mod;
	}
	return ;
}

llsi a[$n], b[$n], c[$n], d[$n];

llsi n, m, c2m;

llsi solve(llsi k) {
	if(k == 0) return !m;
	c2m = c2(m << 1);
	otae(c2m);
	for(int i = 0; i <= k; ++i) a[i] = 1;
	memset(a + k + 1, 0, sizeof(llsi) * (c2m - k - 1));
	b[0] = 1;
	memset(b + 1, 0, sizeof(llsi) * (c2m - 1));
	int ss = n - m + 1;
	while(ss) {
		NTT(a, c2m, 1);
		if(ss & 1) {
			NTT(b, c2m, 1);
			for(int i = 0; i < c2m; ++i) b[i] = (b[i] * a[i]) % mod;
			NTT(b, c2m, -1);
			memset(b + m + 1, 0, sizeof(llsi) * (c2m - m - 1));
		}
		for(int i = 0; i < c2m; ++i) a[i] = (a[i] * a[i]) % mod;
		NTT(a, c2m, -1);
		memset(a + m + 1, 0, sizeof(llsi) * (c2m - m - 1));
		ss >>= 1;
	}
	return b[m];
}

int main() {
//	llsi a[] = {2, 1, 0, 0};
//	otae(4);
//	NTT(a, 4, 1);
//	for(int i = 0; i < 4; ++i) a[i] = a[i] * a[i] % mod;
//	NTT(a, 4, -1);
//	for(int i = 0; i < 4; ++i) std::cout << a[i] << char(i == 3 ? 10 : 32);
//	return 0;
	llsi k;
	std::cin >> n >> m >> k;
	if(n < m || m < k) std::cout << "0\n";
	else               std::cout << (solve(k) - solve(k - 1) + mod) % mod << char(10);
	return 0;
}

Postscript

珍爱生命,远离数数题