1. 程式人生 > 其它 >NOIP模擬88(多校21)

NOIP模擬88(多校21)

「按位或·最短路徑·仙人掌·對弈」on 11.2

前言

對於這套題的總體感覺就是難,然後就是自己很菜。。。

對於 T1 考試時只會一個最垃圾的揹包,考完之後對於思路這一塊也不是很順利,大概這就是薄弱的地方吧。

然後 T2 是比較簡單的一道題了,但是考試的時候只是拿了一點部分分,對於正解的思路也只有一點點。

大概和看錯資料範圍有一點關係吧,主要還是菜。

T3 T4 是那種我最害怕的題目了,T3 一看感覺根本不可做,然後一點思路沒有。

T4 的博弈論更別提,推了個傻瓜特殊性質就跑路了。。。

T1 按位或

解題思路

容斥DP好題。

先來考慮複雜度為 \(3^{log_2t}logn\) 的做法。

那麼由於每一次操作都是等價的因此我們考慮計算出每一次操作可能的貢獻,然後做一個 \(n\)

次方進行求解。

對於 \(t\) 可以揹包 DP 每一個二進位制位,算出對於 \(\bmod\) 3 不同餘數的方案,然後直接整一個 \(n\) 次方???

當然沒有那麼簡單,發現有一些情況是不合法的,有一些二進位制位的組合可能或在一起並不是我們要求的 \(t\) 但是他們每一個數字確實是 3 的倍數。

於是我們需要列舉 \(t\) 的所有子集,進行容斥,最後需要再容斥一下 0 的情況。code

但是顯然這不是正解的複雜度。。。

於是我們可以考慮每一個二進位制位的貢獻,對於 \(2^i\) 如果 \(i\) 是奇數,那麼 \(2^i\;\bmod\;3=2\) 否則就是 1 。

那麼對於所有奇數或者說所有偶數而言他們對於答案的貢獻都是相同的。

於是我們可以直接列舉奇數偶數位選擇多少個,揹包 DP 算出對應方案數,然後類似於上面的不合法情況直接容斥。

最後依舊做一個 \(n\) 次方進行求解,也需要考慮 0 的情況。

code

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define f() cout<<"RP++"<<endl
#define count __builtin_popcount
using namespace std;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=70,mod=998244353;
int n,m,ans,cnt,cnt1,cnt2,f[3],g[3],c[N][N];
int power(int x,int y,int p=mod)
{
	int temp=1; n%=(p-1);
	while(y)
	{
		if(y&1) temp=temp*x%p;
		x=x*x%p; y>>=1;
	}
	return temp;
}
void solve(int x,int y)
{
	f[1]=f[2]=0; f[0]=1;
	for(int i=1;i<=x;i++)
	{
		for(int j=0;j<=2;j++) g[j]=f[j];
		for(int j=0;j<=2;j++) f[(j+1)%3]=(f[(j+1)%3]+g[j])%mod;
	}
	for(int i=1;i<=y;i++)
	{
		for(int j=0;j<=2;j++) g[j]=f[j];
		for(int j=0;j<=2;j++) f[(j+2)%3]=(f[(j+2)%3]+g[j])%mod;
	}
}
#undef int
int main()
{
	#define int long long
	freopen("or.in","r",stdin); freopen("or.out","w",stdout);
	n=read(); m=read();
	int temp=m,pos=0;
	while(temp)
	{
		if(temp&1) cnt++,cnt1+=pos&1;
		pos++; temp>>=1;
	}
	c[0][0]=1;
	for(int i=1;i<=cnt;i++)
	{
		c[i][0]=c[i][i]=1;
		for(int j=1;j<i;j++)
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
	}
	for(int i=1,bas=(cnt&1)?1:-1;i<=cnt;i++,bas=-bas)
		for(int j=0;j<=min(i,cnt1);j++)
			solve(i-j,j),ans=(ans+bas*power(f[0],n)*c[cnt1][j]%mod*c[cnt-cnt1][i-j]%mod+mod)%mod;
	printf("%lld",(ans+((cnt&1)?-1:1)+mod)%mod);
	return 0;
}

T2 最短路徑

解題思路

並不是一道難題。

顯然對於固定的 \(k\) 個點最短路徑的起點終點一定是其中的兩個。

假設最後必須回到起點,一條最短路徑就可以視為遍歷這 \(k\) 個點所需要經過的邊的數量乘上 2 。

那麼加上終點之後呢?我們一定是選擇一對距離最長的點作為終點和起點。

考慮分別計算貢獻,對於第一部分一條邊有貢獻當且僅當它兩邊的兩個部分都有被選中的點,方案數直接組合數計算就好了, \(n\) 不是特別大 \(n\) 或者 \(n^2\) 均可。

對於第二部分,直接暴力列舉所選的點對並且計算可以選的其他點就好了,為了防止重複計算,對於兩個點到某個點距離相同且都是最長路徑的情況我們選擇便後較小來計算貢獻。

對於點對 \((x,y)\) 一個合法的 \(z\) 一定滿足以下條件:

  • \(dis_{x,y}>dis_{x,z}\)

  • \(dis_{x,y}=dis_{x,z} 並且 y<z\)

  • \(dis_{x,y}>dis_{y,z}\)

  • \(dis_{x,y}=dis_{y,z} 並且 x<z\)

假設合法的點有 \(cnt\) 個,那麼這條路徑的貢獻就是 \(\binom{k-2}{cnt}\times len\)

code

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define f() cout<<"RP++"<<endl
using namespace std;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=2e3+10,INF=1e18,mod=998244353;
int n,m,k,root,ans,top,sta[N],s[N],c[N][N],bas[N];
int tim,dfn[N],siz[N],son[N],dep[N],fa[N],topp[N];
int tot=1,head[N],nxt[N<<1],ver[N<<1];
void add_edge(int x,int y){ver[++tot]=y;nxt[tot]=head[x];head[x]=tot;}
void dfs1(int x)
{
	siz[x]=1;
	for(int i=head[x];i;i=nxt[i])
	{
		int to=ver[i]; if(to==fa[x]) continue;
		fa[to]=x; dep[to]=dep[x]+1;
		dfs1(to); siz[x]+=siz[to]; bas[x]+=bas[to];
		if(siz[to]>siz[son[x]]) son[x]=to;
		if(!bas[to]||bas[to]==m) continue;
		ans=(ans+(c[m][k]-c[bas[to]][k]-c[m-bas[to]][k]+2*mod)*2)%mod;
	}
}
void dfs2(int x,int tp)
{
	topp[x]=tp; dfn[x]=++tim;
	if(son[x]) dfs2(son[x],tp);
	for(int i=head[x];i;i=nxt[i])
		if(!dfn[ver[i]]) dfs2(ver[i],ver[i]);
}
int LCA(int x,int y)
{
	while(topp[x]^topp[y])
	{
		if(dep[topp[x]]<dep[topp[y]]) swap(x,y);
		x=fa[topp[x]];
	}
	if(dep[x]>dep[y]) swap(x,y); return x;
}
inline int dist(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}
int power(int x,int y,int p=mod)
{
	int temp=1;
	while(y)
	{
		if(y&1) temp=temp*x%p;
		x=x*x%p; y>>=1;
	}
	return temp;
}
void solve(int x,int y)
{
	int cnt=0,dis=dist(x,y);
	for(int i=1;i<=m;i++)
	{
		if(s[i]==x||s[i]==y) continue;
		if(dist(x,s[i])>dis||dist(y,s[i])>dis) continue;
		if(dist(x,s[i])==dis&&s[i]<y) continue;
		if(dist(y,s[i])==dis&&s[i]<x) continue;
		cnt++;
	}
	if(cnt>=k-2) ans=(ans-dis*c[cnt][k-2]%mod+mod)%mod;
}
#undef int
int main()
{
	#define int long long
	freopen("tree.in","r",stdin); freopen("tree.out","w",stdout);
	n=read(); m=read(); k=read();
	for(int i=1;i<=m;i++) s[i]=read(),bas[s[i]]=1;
	for(int i=1,x,y;i<n;i++) x=read(),y=read(),add_edge(x,y),add_edge(y,x);
	c[0][0]=1; for(int i=1;i<=n;i++) c[i][0]=c[i][i]=1;
	for(int i=1;i<=n;i++) for(int j=1;j<i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
	dfs1(1); dfs2(1,1); sort(s+1,s+m+1);
	for(int i=1;i<=m;i++) for(int j=i+1;j<=m;j++) solve(s[i],s[j]);
	printf("%lld",ans*power(c[m][k],mod-2)%mod);
	return 0;
}

T3 仙人掌

神仙仙人掌+行列式,咕了

大坑未補

T4 對弈

解題思路

又是 老臉買原題 系列

有一個性質: 在最優策略下,如果可以, Alice 只會向右移動, Bob 只會向左移動。

證明:記 \(a_i\) 表示第 \(i\) 個紅棋的位置, \(b_i\) 表示第 \(i\) 個藍棋的位置,我們將一個狀態表示為一個 \(\frac{k}{2}\) 元組 \((b_1-a_1-1,b_2-a_2-1...b_{\frac{k}{2}}-a_{\frac{k}{2}}-1)\) 。記 \(d_i=b_i-a_i-1\) ,如果 Alice 將 \(a_i\) 向左移動,那麼在它右邊的那個藍棋 \(b_i\) 一定可以移動相同距離,使得對應的 \(d_i\) 不變,但是顯然 \(a_i\) 的移動範圍變小了,不這麼做顯然不會更劣。

在這個性質的前提下 \(d_i\) 只可能會越來越小,就變成了一個取石子游戲,也就是(下文中的變數名可能與上文沒有關係):

\(n\) 堆石子,每堆有 \(a_i\) 個,每次可以選 \(1\sim m\) 堆,從這幾堆中各取走若干石子,每堆至少取一個。若輪到某人時,所有石子都已經被取完了,這個人就輸了。

那麼必敗局面當且僅當將所有 \(a_i\) 轉成二進位制後,每一位 1 的個數 \(\bmod\;(m+1)=0\)

證明

  1. 全為 0 的局面一定是必敗態。

  2. 任何一個 \(N\) 狀態(必敗狀態),經過一次操作以後必然會到達 \(P\) 狀態(必勝狀態)。在某一次移動中,至少有一堆被改變,也就是說至少有一個二進位制位被改變。由於最多隻能改變 \(m\) 堆石子,所以對於任何一個二進位制位, 1 的個數至多改變 \(m\) 。而由於原先的總數為 \(m+1\) 的整數倍,所以改變之後必然不可能是 \(m+1\) 的整數倍。故在 \(N\) 狀態下一次操作的結果必然是 \(P\) 狀態。

  3. 任何 \(P\) 狀態,總有一種操作使其變化成 \(N\) 狀態。從高位到低位考慮所有的二進位制位。假設用了某種方法,改變了 \(k\) 堆,使第 \(i\) 位之前的所有位的 1 的個數都變成 \(m+1\) 的整數倍。現在要證明總有一種方法讓第 \(i\) 位也變成 \(m+1\) 的整數倍。顯然,對於已經改變的那 \(k\) 堆,當前位可以自由選擇 0 或 1 。設除去已經更改的 \(k\) 堆,剩下的堆第 \(i\) 位上 1 的總和 \(\bmod\;(m+1)=sum\) ,分類討論:

  • \(sum\le m-k\) 此時可以將這些堆上的 1 全部拿掉,然後讓那 \(k\) 堆的第 \(i\) 位全部置成 0 。
  • \(sum>m-k\) 此時我們在之前改變的 \(k\) 堆中選擇 \(m+1-sum\) 堆,將他們的第 \(i\) 位置成 \(i\) ,剩下的置成 \(0\) 。由於 \(m+1-sum\le k\) ,故這是可以達到的。

那麼就可以 DP 了 \(f(i,j)\) 表示考慮了前 \(i\) 位,當前總和為 \(j\) 的方案數。

於是從 \(\frac{k}{2}\) 堆裡面選出 \(num\times (m+1)\) 個並且這些石子的第 \(i\) 位為 1 。

並且我們還要選出這幾堆的位置,於是方程就是:

\[f(\;i+1,j+2^i\times num\times (m+1)\;)+=f(i,j)\times\binom{num\times(m+1)}{\frac{k}{2}} \]\[f(i,j)=f(i,j)\times \binom{n-i-\frac{k}{2}}{\frac{k}{2}} \]

最後拿總方案數一減就好了。

code

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define f() cout<<"RP++"<<endl
using namespace std;
inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=3e4+10,Lg=70,mod=1e9+7;
int ans,n,m,k,lg,fac[N],ifac[N],f[Lg][N];
int power(int x,int y,int p=mod)
{
	int temp=1;
	while(y)
	{
		if(y&1) temp=temp*x%mod;
		x=x*x%mod; y>>=1;
	}
	return temp;
}
void add(int &x,int y){x+=y;if(x>mod)x-=mod;}
int C(int x,int y){if(x<y)return 0;return fac[x]*ifac[y]%mod*ifac[x-y]%mod;}
#undef int
int main()
{
	#define int long long
	freopen("chess.in","r",stdin); freopen("chess.out","w",stdout);
	n=read(); k=read(); m=read(); lg=log2(n)+1; f[0][0]=1;
	fac[0]=ifac[0]=1; for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
	ifac[n]=power(fac[n],mod-2); for(int i=n-1;i>=1;i--) ifac[i]=ifac[i+1]*(i+1)%mod;
	for(int i=0;i<=lg;i++)
		for(int j=0;j<=n-k;j++)
			for(int p=0;;p++)
				if((p<<i)*(m+1)>n-k||p*(m+1)>k/2) break;
				else add(f[i+1][j+(p<<i)*(m+1)],f[i][j]*C(k/2,p*(m+1))%mod);
	for(int i=0;i<=n-k;i++) add(ans,f[lg][i]*C(n-i-k/2,k/2)%mod);
	printf("%lld",(C(n,k)-ans+mod)%mod);
	return 0;
}