1. 程式人生 > 其它 >Atcoder Grand Contest 054 題解

Atcoder Grand Contest 054 題解

那天晚上由於畢業晚會與同學吃飯喝酒沒打 AGC,第二天稍微補了下題,目前補到了 E,顯然 AGC 的 F 對於我來說都是不可做題就沒補了(bushi

A

簡單題,不難發現如果我們通過三次及以上的操作將這個串消完,那麼我們完全可以把它壓縮到兩次以內,因此如果兩段字元不同答案就是 \(1\),否則我們列舉分割點然後判斷分割點兩段是否都可以一次消完,如果存在這樣的分割點答案就是 \(2\),否則答案為 \(-1\)

B

注意到如果我們將原序列分成和相等的兩部分並兩部分將它們排成一列,那麼有且只有一種排列方式對應出來是這個序列,具體證明就哪邊小就往哪邊塞即可,這個很好理解,因此我們可以設 \(dp_{i,j,k}\)

表示在前 \(i\) 個數裡面選擇了 \(k\) 個數,和為 \(j\) 的方案數,轉移就正常按照揹包的套路轉移即可,記 \(S=\sum\limits_{i=1}^nw_i\),那麼最終答案即為 \(\sum\limits_{i=1}^{n-1}dp_{n,S/2,i}·i!·(n-i)!\)。時間複雜度 \(\mathcal O(n^4)\)

C

帶家好,這就是一個猜出結論然後沒敢寫的 sb,晚上交了一發,竟然過了

知乎習慣:先拋結論,再給證明。\(S\) 為滿足逆序對數為 \(k\) 的下標的集合,那麼答案即為 \(\prod\limits_{x\in S}(n-x+1)\)

證明:這是我自己 yy 出來的一個證明,錯了不要打我

首先考慮對於一個固定的排列 \(p\),最優策略長什麼樣,顯然每次交換最多使得某個 \(p_i\) 的逆序對數減 \(1\),我們記 \(p_i\) 的逆序對數為 \(x_i\),故交換次數有下界 \(\sum\limits_{i=1}^n\max(x_i-k,0)\),而如果我們每次都找到一個滿足 \(x_i>k\)\(p_{i-1}>p_i\) 的下標 \(i\) 並交換 \(p_{i-1},p_i\),那麼每次交換都能使 \(x_i>k\) 的逆序對數恰好 \(-1\),也就能達到這個下界。那麼問題又來了,是否每次都能找到這樣的下標 \(i\) 呢?答案是肯定的,考慮反證法,如果對於所以 \(x_i>k\)

\(i\) 都有 \(p_{i-1}<p_i\),那麼由於 \(p_{i-1}<p_i\),必然有 \(x_{i-1}\ge x_i>k\),同理可得 \(p_{i-2}<p_i,x_{i-2}\ge x_{i-1}\ge x_i>k\),如此一直推下去可以推得 \(p_1<p_2<p_3<\cdots<p_i\),即 \(x_i=0\),而 \(k\ge 0\),矛盾!因此我們有了以下演算法:

Algorithm. 每次找到一個滿足 \(x_i>k,p_{i-1}>p_i\) 的下標 \(i\) 並交換 \(p_{i-1},p_i\),如此進行下去直到不存在 \(x_i>k\) 即可。

接下來考慮原問題,假設滿足 \(x_i=k\) 的下標集合為 \(S=\{i_1,i_2,\cdots,i_k\}(i_m<i_{m+1},m\in[1,k-1])\),那麼我們只需證明對於任意將 \(S\) 中的元素相對位置向右移的方案都有它還原出來的排列為 \(P'\)

由於對於最終排列 \(P'\) 而言都有 \(x_i\le k\),因此 \(p'_{i_t}\) 必然小於 \(p'_{i_t+1},p'_{i_t+2},\cdots,p'_n\),否則假設 \(p'_j<p'_{i_t},j>i_t\),那麼 \(j\) 的逆序對數大於 \(x_{i_t}=k\),矛盾。又 \(i_m<i_{m+1},m\in[1,k-1]\),故我們有 \(p'_{i_m}<p'_{i_{m+1}}\),我們考慮原排列中 \(p'_{i_1}\) 的位置 \(v\),顯然根據上面的推論如果 \(v\ne i_1\) 必然有 \(x_v>k\)\(\forall j\in[i_1,v-1],p_j>p_{i_1}\),因此我們會一直向左移動 \(i_1\) 直到它的逆序對數 \(=k\) 為止,而顯然這個終止位置就是最後的 \(i_1\),如此繼續下去,還原 \(i_2,i_3,\cdots,i_m\) 即可得到排列 \(P'\)(當然我這裡證明比較玄乎,如果嚴謹證明的話可用歸納法表述)

時間複雜度 \(\mathcal O(n\log n)\)

最慢的一個點跑了 7ms,也太慢了吧(說反話 ing

D

神仙題。

首先看到最少交換次數可以自然想到 \(dp\)\(dp_{i,j,k,l}\) 表示填了 \(i\)(\(j\))\(k\)o\(l\)x 的最少交換次數,根據這題的套路,最小的交換次數就是原序列與交換後的序列中相對位置發生改變的字元對數,也就是說加入一個字元後對交換次數的貢獻是可以 \(\mathcal O(1)\) 求出來的,然鵝並沒有什麼卵用,這個狀態就已經達到了 \(n^4\),一臉過不去。

於是我們不妨來分析一下這個東西有什麼性質,看到括號序列我們可以很自然地將左括號當作 \(1\),右括號當作 \(-1\),那麼我們可以將這四個字元分為兩類,()(表示的數為 \(0\))和 ox(對應的數為 \(0\)),那麼可以發現以下性質:

Observation 1. 我們顯然不會交換 ox

證明:如果我們交換的 ox 這類字元中有 o,由於 o 展開來是 (),本來就是一個合法的字串,因此 o 的位置並不影響括號串的合法性,而如果我們交換的兩個字元都是 x 那我們交換它們幹嘛呢

這樣一來我們假設括號序列的順序為 \(S\),那麼括號序列內部的交換次數顯然是可以掃一遍求出的,而 ()ox 兩類字元對答案的貢獻是可以 \(dp\) 的,\(dp_{i,j}\) 表示填好了 \(S\) 的前 \(i\) 位和 ox 的序列的前 \(j\) 位的最少交換次數,這個 \(dp\) 顯然是可以在常數時間內轉移的,注意如果括號序列字首和為 \(0\) 就不能放 x。因此這部分時間複雜度是 \(\mathcal O(\dfrac{n^2}{4})\) 的,沒有問題,也就是說如果我們能確定 \(S\) 的順序那這題就做完了。

那怎麼確定 \(S\) 的順序呢?

Observation 2. 最終的括號序列一定是在原括號順序的前提下,使用最少交換次數交換求得的括號序列

說人話,就是如果原來左括號右括號順序是 \(S'\),那麼設我們通過最少次交換使 \(S'\) 變成合法括號序列後,\(S'\) 變成了 \(S\),那麼 \(S\) 就是最終括號串的順序。

為什麼?這裡再給出一個更不嚴謹的證明。不難發現我們交換括號序列的原因只有一點,就是使得不能放 x 的地方可以放 x,但事實上我們與其交換這些括號,還不如交換這些 x 們,把它們放到括號中來得更優,比方說 ()xxxx(),如果我們交換括號的話要將最後一個 x 後面的 ( 移動五格,而如果我們移動 x 只要將每個 x 向右移動 \(1\) 格,總共四步,因此我們大可不必花這些精力移動括號。(真是玄乎到不能再玄乎了呢,哪位好心的鴿鴿知道這東西的 proof 麻煩來教教蒟蒻唄/kel)

於是這題就真的做完了(

const int MAXN=8e3; 
int n,n1,n2,ans=0,ps1[MAXN+5],ps2[MAXN+5];
char s[MAXN+5],s1[MAXN+5],s2[MAXN+5];
int dp[MAXN+5][MAXN+5],pre1[MAXN+5],pre2[MAXN+5],preo[MAXN+5],prex[MAXN+5];
int sum1[MAXN+5],sum2[MAXN+5],sumo[MAXN+5],sumx[MAXN+5];
int main(){
	scanf("%s",s+1);n=strlen(s+1);
	for(int i=1;i<=n;i++) (s[i]>97)?(s2[++n2]=s[i],ps2[n2]=i):(s1[++n1]=s[i]);
	for(int i=1;i<=n;i++){
		pre1[i]=pre1[i-1]+(s[i]=='(');
		pre2[i]=pre2[i-1]+(s[i]==')');
		preo[i]=preo[i-1]+(s[i]=='o');
		prex[i]=prex[i-1]+(s[i]=='x');
	} //cout<<s1+1<<endl;
	for(int i=1,s=0;i<=n1;i++){
		s+=1-((s1[i]==')')<<1);
		if(s<0){
			s=1;
			for(int j=i+1;j<=n1;j++) if(s1[j]=='('){
//				printf("%d %d\n",j,i);
				swap(s1[j],s1[i]);ans+=j-i;break;
			}
		}
	} memset(dp,63,sizeof(dp));dp[0][0]=0;
//	cout<<s1+1<<endl;printf("%d\n",ans);
	for(int i=1,p1=0,p2=0;i<=n1;i++){
		if(s1[i]=='('){
			do{p1++;} while(s[p1]!='(');
			ps1[i]=p1;
		} else {
			do{p2++;} while(s[p2]!=')');
			ps1[i]=p2;
		}
	}
	for(int i=1;i<=n1;i++){
		sum1[i]=sum1[i-1]+(s1[i]=='(');
		sum2[i]=sum2[i-1]+(s1[i]==')');
	}
	for(int i=1;i<=n2;i++){
		sumo[i]=sumo[i-1]+(s2[i]=='o');
		sumx[i]=sumx[i-1]+(s2[i]=='x');
	}
//	for(int i=1;i<=n1;i++) printf("%d%c",ps1[i]," \n"[i==n1]);
//	for(int i=1;i<=n2;i++) printf("%d%c",ps2[i]," \n"[i==n2]);
//	for(int i=1;i<=n;i++) printf("%d%c",pre1[i]," \n"[i==n]);
//	for(int i=1;i<=n;i++) printf("%d%c",pre2[i]," \n"[i==n]);
//	for(int i=1;i<=n;i++) printf("%d%c",prex[i]," \n"[i==n]);
//	for(int i=1;i<=n;i++) printf("%d%c",preo[i]," \n"[i==n]);
	for(int i=0,s=0;i<=n1;i++){
		for(int j=0;j<=n2;j++){
			if(i^n1) chkmin(dp[i+1][j],dp[i][j]+max(sumo[j]-preo[ps1[i+1]],0)+max(sumx[j]-prex[ps1[i+1]],0));
			if(j^n2&&(s||(s2[j+1]=='o'))) chkmin(dp[i][j+1],dp[i][j]+max(sum1[i]-pre1[ps2[j+1]],0)+max(sum2[i]-pre2[ps2[j+1]],0));
		} s+=1-((s1[i+1]==')')<<1);
	} printf("%d\n",ans+dp[n1][n2]);
//	for(int i=0;i<=n1;i++) for(int j=0;j<=n2;j++) printf("%d %d %d\n",i,j,dp[i][j]);
	return 0;
}

E

知乎好習慣:只拋結論,不給證明,因為我不會證明,Atcoder 題解也寫得不是太詳細

Observation. 以 \(p_1<p_n\) 的情況為例,序列合法當且僅當 \(\exists k\in[1,n-1]\) 滿足 \(p_k\le p_1,p_{k+1}\ge p_n\)

證明不會

考慮怎樣計算這個東西,咱們不妨從反面入手,拿總方案減去不合法的方案數,記 \(f(n,A)\) 表示 \(p_1=A,p_n>p_1\) 的不合法的序列的個數,那麼 \(ans=(n-1)!-f(n,A)-f(n,n-A)\),因此我們只需求出 \(f(n,A)\) 即可。

考慮怎樣求 \(f(n,A)\),我們假設 \(p_n=p_1+k\),那麼 \((p_1,p_n)\)\(k-1\) 個數隨便填,\((k-1)!\) 種可能,當我們依次填入 \([1,p_1)\) 時,填入 \(i\) 時有 \(k-2+i\) 種填法,再依次填入 \((p_n+1,n]\) 時,填入 \(p_n+i\) 時有 \(k-2+i\) 種填法,乘在一起是 \((k-1)!\times\dfrac{(k-2+A-1)!}{(k-2)!}\times\dfrac{(k-2+n-A-k)!}{(k-2)!}=(A+k-3)!\times(n-A-2)!\times\dfrac{1}{(k-2)!}\times(k-1)\),因此填法總數就是 \(\sum\limits_{2\le k\le n-A}(A+k-3)!\times(n-A-2)!\times\dfrac{1}{(k-2)!}\times(k-1)=(n-A-2)!\sum\limits_{2\le k\le n-A}\dbinom{A+k-3}{k-2}(k-1)\),再推個柿子可以得到:

\[\begin{aligned} ans&=(n-A-2)!\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}(k+1)\\ &=(n-A-2)!(\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}k+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k})\\ &=(n-A-2)!(\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k-1}A+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{k}))\\ &=(n-A-2)!(A·\sum\limits_{0\le k\le n-A-3}\dbinom{A+k}{A}+\sum\limits_{0\le k\le n-A-2}\dbinom{A+k-1}{A-1})\\ &=(n-A-2)!(A·\dbinom{n-2}{A+1}+\dbinom{n-2}{A}) \end{aligned} \]

大功告成。

搬 Atcoder 題解 ing

const int MAXN=2e6;
const int MOD=998244353;
int fac[MAXN+5],ifac[MAXN+5];
void init_fac(int n){
	for(int i=(fac[0]=ifac[0]=ifac[1]=1)+1;i<=n;i++) ifac[i]=1ll*ifac[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*ifac[i]%MOD;
}
int binom(int x,int y){
	if(x<0||y<0||x-y<0) return 0;
	return 1ll*fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;
}
int calc(int n,int A){
	return 1ll*fac[n-A-2]*fac[A-1]%MOD*((1ll*A*binom(n-2,A+1)+binom(n-2,A))%MOD)%MOD;
}
void solve(){
	int n,A;scanf("%d%d",&n,&A);
	printf("%d\n",(0ll+fac[n-1]-calc(n,A)-calc(n,n-A+1)+MOD+MOD)%MOD);
}
int main(){
	init_fac(MAXN);
	int qu;scanf("%d",&qu);while(qu--) solve();
	return 0;
}