[Tricks-00004]CF1954F(自己胡的 trick,被 Burnside 完爆)

maihe發表於2024-11-17

介紹下自己的離奇思路:

先讀清楚題意!要求是旋轉等價,即兩個以 \(c\)\(1\) 開頭,總 \(1\) 個數不超過 \(k+c\) 的字串算一種。

那怎麼刻畫"只算一種"這個條件呢?一個想法可以是,對每個字串賦一個權值,一種字串的權值即旋轉出來的每個合法的,把它們加起來應該是 \(1\),再全部加出來。

一個直接的賦權方式是,對一個最小迴圈節是 \(d(d|n)\) 的字串,它的權值為 \(\dfrac{1}{\sum\limits_{i=1}^{d}ok_i}\),其中 \(ok_i\) 代表字串是否可以以位置 \(i\) 開頭。

然後做法就是列舉 \(d\),求出容斥係數,因為要考慮所有大小為 \(d\)\(01\) 串,所以要 dp 三個量:長度,\(1\) 個數,以及合法開頭點數。這樣複雜度肯定沒救了。

這是同學你可能會說:沒必要讓每個"合法點"都合法作為開頭啊?我對於一個 \(len\geq c\)\(1\) 段,要求上必須以這個段的段頭作為開頭才可以,這不就輕鬆了嗎?

很好!這樣就改為了段數分之一,不過還是要 dp 這個段數有點煩,怎麼辦呢?

其實這時候你可以發現,我們沒有必要非去設它的所有合法輪換的權值都相等啊!只需要加起來是 \(1\) 就可以。於是容易想到,停時定理那個設權值的方式,即 \(\dfrac{a_i}{\sum a_k}\),換在這題也就是,目前一個段到下一個合法段的距離,除掉總長,也就是 \(d\)。而目前段就是 \(1\sim c\) 開頭那個儘量延伸,且要求 \(s_d=0\),即必須要是一個段頭。

這下我們就不需要考慮這個字串整體了,只需要得到一個最前的合法段,欽定即可!

其實可以兩種情況分開討論:

第一種情況的權值是 \(\dfrac{x-1}{d}\),其中 \(x\) 表示後面那個合法段的開頭位置。第二種的權值就是 \(\dfrac{d}{d}=1\)

顯然情況一更難,我們只考慮這個咋求。不難發現,列舉 \(x\) 的位置,和 \(x\) 前面 \(0\) 的個數,就可以透過一些預處理做到除了預處理之外單次 \(O(d^2)\) 的複雜度。

那麼預處理是什麼呢?也不難,就是 \(f_{i,j}\) 表示長度為 \(i\),以 \(0\) 結尾,不能出現連續 \(c\)\(1\) 的字串數,則有轉移:

\[f_{i,j}=\sum\limits_{k=0}^{c-1}f_{i-k-1,j-1} \]

其實這個甚至只用 \(+1\) 最佳化都可以做,完全不需要字首和最佳化。不過其它一些預處理可能要字首和最佳化,不過都很簡單。這裡是 \(O(n^2)\) 的。

最後一個小問題是我們列舉的是 \(d\),也就是欽定了有大小為 \(d\) 的迴圈節。注意,最小迴圈節一定整除其它的,所以容斥係數可以直接這麼理解:

\[f_d=\dfrac{n}{d}-\sum\limits_{d|g|n\land g\neq d}f_g \]

然後乘乘加加就可以了。複雜度是 \(O(n^2+\sum\limits_{d|n}d^2)=O(\sum\limits_{i\geq 1}\dfrac{n}{i}^2)=O(n^2)\)

這是我的做法,程式碼如下,很短:

#include<bits/stdc++.h>
#define mod 1000000007
using namespace std;
int powdv(int x,int y=mod-2){
	int ans=1;
	while(y){
		if(y&1)ans=1ll*ans*x%mod;
		y>>=1,x=1ll*x*x%mod;
	}
	return ans;
}
int ff[3005],hh[3005];
int f[3005][3005],h[3005][3005];
int main(){
	int n,c,k;
	scanf("%d%d%d",&n,&c,&k);
	for(int d=n;d>=1;--d)if(n%d==0){
		ff[d]=powdv(d);
		for(int g=2*d;g<=n;g+=d)if(n%g==0){
			ff[d]=(ff[d]-ff[g]+mod)%mod;
		}
	}
	f[1][1]=1;
	for(int i=2;i<=n;++i)for(int j=2;j<=i;++j){
		f[i][j]=(f[i-1][j]+f[i-1][j-1])%mod;
		if(i>c+1)f[i][j]=(f[i][j]-f[i-c-1][j-1]+mod)%mod;
	}
	h[0][0]=1;
	for(int i=1;i<=n;++i)for(int j=0;j<=i;++j){
		h[i][j]=(h[i-1][j]+(j?h[i-1][j-1]:0))%mod;
	}
	for(int i=1;i<=n;++i)for(int j=i;j>=1;--j){
		h[i][j-1]=(h[i][j-1]+h[i][j])%mod;
	}
	int ans=0;
	for(int d=c+1;d<=n;++d)if(ff[d]){
		int mx=(k+c)/(n/d),gs=max(0,d-mx),he=0;
		for(int i=1;i<=d-c;++i)for(int j=gs;j<=i;++j){
			he=(he+1ll*d*f[i][j])%mod;
		}
		for(int i=1;i<=d;++i)hh[i]=0;
		for(int i=c+2;i<=d-c;++i){
			int ht=0;
			for(int j=1;j<=i;++j){
				hh[j]=(hh[j]+f[i-c-1][j])%mod;
				ht=(ht+1ll*hh[j]*h[d-c-i][max(0,gs-1-j)])%mod;
			}
			he=(he+1ll*(i-1)*ht)%mod;
		}
		ans=(ans+1ll*ff[d]*he)%mod;
	}
	if(k==n-c)ans=(ans+1)%mod;
	printf("%d\n",ans);
	return 0;
}

另外說說 Burnside 的做法,也是積累了一個套路:

------------------分割線--------------------

看了眼題解,怎麼這麼簡單???我最開始的時候想用 Burnside,可是發現不完備,遂放棄。實際你仔細想想,發現我只要不要求必須以 \(c\)\(1\) 開頭就好了啊!!!!!只用這樣一件事情:存在相鄰 \(c\) 個字元全為 \(1\),之後就還是列舉迴圈節去 Burnside 就好了。思維難度少了 \(114514\) 倍,唉唉唉。

相關文章