經典
我們先來解決最經典的圓環染色問題。
一個環上有\(n\)個點,每個點染為\(m\)種顏色之一,要求相鄰兩點顏色不同。求可行的方案數。
這里有一道題的部分分是這個問題:uoj#241. 【UR #16】破壞發射台
——《彩色圓環(circle)》命題報告,吳佳俊
那么,設\(f[i][0/1]\)表示當前正在決定第\(i\)位的顏色,且要求該顏色是否(\(0/1\))與第\(1\)位顏色相同。
對於\(f[i][1]\)沒啥好決定的,第\(i\)位必須與第\(1\)位相同,所以系數是\(1\)。
對於\(f[i][0]\)分兩種情況,一種前接\(f[i-1][1]\),這時第\(i-1\)位顏色與第\(1\)位顏色相同,有\((m-1)\)種顏色供第\(i\)位選擇。一種是前接\(f[i-1][0]\),第\(i-1\)位與第\(i\)位不同了,第\(i\)位不能與其中任一相同,只有\((m-2)\)種可以選。
初始狀態很重要,保險的定義應該從\(2\)開始,但是根據意義從\(1\)開始也無妨。
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
typedef long long ll;
const ll MOD=998244353;
const ll MXN=1E7+5;
ll f[MXN][2];
int main(){
ll N,M;scanf("%lld%lld",&N,&M);
f[1][0]=0,f[1][1]=M;
for(ll i=2;i<=N;i++){
f[i][0]=((M-1)*f[i-1][1]+(M-2)*f[i-1][0])%MOD;
f[i][1]=f[i-1][0]%MOD;
}
cout<<f[N][0];
return 0;
}
上面這個dp其實還可以更優,用矩陣快速冪可以優化,也可以用特征根等方式推出通項公式\((m-1)^n+(m-1)(-1)^n\)。
下面介紹幾種變種,本質其實是一樣的,可以根據題目靈活選擇。
可以考慮欽定第\(1\)位的顏色,把枚舉第\(1\)位顏色放在求答案部分。
可以考慮假設出一個第\(0\)位的顏色,這樣環的要求變為第\(0\)為與末位相同,即答案變為\(f[n][1]\)。好處在於可以將初始狀態提前到第\(0\)位設置。
還有另一種dp的方式是欽定當前位的顏色,考慮前一位可以選那些顏色。狀態轉移方程是:
既然是欽定,答案就需要另外乘\(M\)來枚舉顏色。
總之,都是拆環為鏈,壓縮無用狀態,用一個\(0/1\)位保留環的限制。
我們從中獲取了一種處理這類環上dp的思路,即增設0/1位來維護首尾信息
利用該模型,可以解決許多變種問題。
破壞發射台
一句話題意:長度為 \(n\) 的環,每個點染色,有 \(m\) 種顏色,要求相鄰相對不能同色,求方案數對 \(998244353\) 取模的結果。(定義兩個點相對為去掉這兩個點后環能被分成相同大小的兩段)
\(n,m \le 10^9\)
對於長度為奇數的環,就是經典問題,矩陣快速冪或者直接通項公式即可。
對於長度為偶數的環,就有點復雜了。因為要考慮相對點之間的相互影響,不妨將它們捆在一塊,裝在一個狀態里考慮。然后,我們需要處理環的上半部分和下半部分的相互接觸問題,類比處理經典問題的思路,
我們設第一格的顏色為 \(A\),設第 \(n/2+1\) 格的顏色為 B,然后設個二元三進制狀態表示第 i 格和第 \(n/2+i\) 格的顏色是否為顏色 A 或顏色 B(\(1≤i≤n/2\))。
設 \(F[i][0..8]\) 表示推到第 \(i\) 格的所有二元三進制狀態的合法方案數,然后遞推一波即可。
——UOJ Round #16 題解
這個討論有點變態,,,代碼就咕了(
彩色圓環
試題來源
2010中國國家集訓隊命題答辯
問題描述
小A喜歡收集寶物。一天他得到了一個圓環,圓環上有N顆彩色寶石,閃閃發光。小A很愛惜這個圓環,天天把它帶在身邊。
一天,小A突然發現圓環上寶石的顏色是會變化的。他十分驚訝,仔細觀察這個圓環后發現,圓環上寶石的顏色每天變化一次,而且每顆寶石的顏色都等概率地為特定的M種顏色之一。小A發現了這個秘密后,對圓環更是愛不釋手,時時刻刻都在研究。
又經過了一段時間,小A發現因為圓環上寶石的顏色不斷變化,圓環有時會顯得比其他時候更美麗。為了方便比較,小A這樣定義圓環的“美觀程度”:
設圓環上相同顏色的寶石構成的連續段長度分別為a1, a2, ..., an;
定義圓環的“美觀程度” \(R = \prod_{i=1}^{n} a_i\) 。以圖一給出的圓環為例,有a1 = 3, a2 = 2, a3 = 1,故R = 6。
現在小A想知道,在上述前提下,圓環的“美觀程度”的期望值E(R)是多少。因為如果知道了E(R),他就可以判斷每天變化出的新圓環是否比一般情況更美麗。
說明:“美觀程度”的期望值即為對每種可能的圓環狀態的“美觀程度”與其出現概率的乘積進行求和所得的值。輸入格式
輸入僅有一行,該行給出依次兩個正整數N, M,分別表示寶石的個數和寶石在變化時可能變成的顏色種類數。
輸出格式
輸出應僅有一行,該行給出一個實數E(R),表示圓環的“美觀程度”的期望值。
樣例輸入
3 2
樣例輸出
2.25
樣例輸入
200 1
樣例輸出
200
數據規模和約定
20%的數據滿足1 ≤ N, M ≤ 8;
50%的數據滿足1 ≤ N, M ≤ 25;
100%的數據滿足1 ≤ N ≤ 200, 1 ≤ M ≤10^9。
先來看鏈的情況
設\(f[i]\)表示考慮到第\(i\)位時的期望美觀度,按划分顏色塊的思路dp,顯然有
其中\(P[i]\)表示連續選\(i\)個相同一種顏色的概率
\((M-1)\)代表當前顏色塊的顏色要與前塊不同
那么現在用圓環染色的思路來試着寫環的dp式
正如解決原始版本的方式,我們拆環為鏈,並假設已經欽定了第\(0\)位的顏色。我們設\(f[i][0/1]\)表示考慮前\(i\)位,且要求第\(i\)位(所屬塊)顏色是否(\(0/1\))與第\(0\)位顏色相同,這時的期望美觀度。可得轉移方程:
考慮如何求答案。由於無法直接獲取首尾相接顏色塊長度,考慮將它單獨拎出來計算。枚舉首尾相接顏色塊兩端加起來的總長度\(x\),則總共有\(x\)種分割首尾的方案,每種方案有\(M\)個顏色可以選擇(因為欽定),每個方案貢獻為\(x\),剩下的部分就可以用\(f\)來表示了。(想想欽定第\(0\)位而不是第\(1\)位的目的)
\(x=N\)時要特判,於是答案如下
\(O(n^2)\)的代碼
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<queue>
#include<vector>
using namespace std;
typedef long double ldb;
typedef long long ll;
const ll MXN=1005;
ll N,M;
ldb f[MXN][2];
ldb P[MXN];
int main(){
cin>>N>>M;
P[0]=1;for(ll i=1;i<=N;i++) P[i]=P[i-1]/M;
f[0][0]=0;f[0][1]=1;//f[0]時只有第0位,一定相同,故f[0][0]不合法置0,f[0][1]置單位元
for(ll i=1;i<=N;i++){
f[i][0]=f[i][1]=0;
for(ll j=0;j<i;j++){//可以從0轉移,給了只有一個塊轉移的機會
f[i][0]+=f[j][0]*(i-j)*P[i-j]*(M-2)
+f[j][1]*(i-j)*P[i-j]*(M-1);
f[i][1]+=f[j][0]*(i-j)*P[i-j];
}
}
ldb ans=N*P[N]*M;
for(ll x=1;x<N;x++)
ans+=x*x*P[x]*M*f[N-x][0];//一個x是貢獻,一個x是分割開頭和結尾的方式數,f[N-x][0]則充當了中間部分
printf("%.5Lf",ans);
return 0;
}
我們發現推出的dp方程有一部分是與\(j\)無關的。將它們提出來,維護剩下的只與\(j\)有關的前綴和,復雜度即可降至\(O(N)\)
前綴和優化后\(O(n)\)
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<queue>
#include<vector>
using namespace std;
typedef long double ldb;
typedef long long ll;
const ll MXN=1000005;
ll N,M;
ldb f[MXN][2];
ldb powM[MXN];//M^i
int main(){
cin>>N>>M;
powM[0]=1;for(ll i=1;i<=N;i++) powM[i]=powM[i-1]*M;
f[0][0]=0;f[0][1]=1;
ldb s_01=0,s_0j=0;
ldb s_11=1,s_1j=0;
for(ll i=1;i<=N;i++){
f[i][0] = s_01*(M-2)*i/powM[i] + s_0j*(M-2)/powM[i]
+ s_11*(M-1)*i/powM[i] + s_1j*(M-1)/powM[i];
f[i][1] = s_01 *i/powM[i] + s_0j /powM[i];
s_01 += f[i][0]*powM[i];
s_0j += f[i][0]*powM[i]*i;
s_11 += f[i][1]*powM[i];
s_1j += f[i][1]*powM[i]*i;
}
ldb ans=N/powM[N]*M;
for(ll x=1;x<N;x++)
ans+=x*x/powM[x]*M*f[N-x][0];
printf("%.5Lf",ans);
return 0;
}
實際上是會炸精度的,懶得管了:p
2020/02/04