淺析一類要求相鄰不同的環上染色問題


經典

我們先來解決最經典的圓環染色問題。

一個環上有\(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)\)種可以選。

\[\begin{aligned} f[i][0] &= (m-2) f[i-1][0] + (m-1) f[i-1][1] \\ f[i][1] &= f[i-1][0] \end{aligned} \]

初始狀態很重要,保險的定義應該從\(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的方式是欽定當前位的顏色,考慮前一位可以選那些顏色。狀態轉移方程是:

\[\begin{aligned} f[i][0] &= f[i-1][1] + (m-2) * f[i-1][0] \\ f[i][1] &= (m-1) * f[i-1][0] \end{aligned} \]

既然是欽定,答案就需要另外乘\(M\)來枚舉顏色。

總之,都是拆環為鏈,壓縮無用狀態,用一個\(0/1\)位保留環的限制。

我們從中獲取了一種處理這類環上dp的思路,即增設0/1位來維護首尾信息

利用該模型,可以解決許多變種問題。

破壞發射台

uoj#241. 【UR #16】破壞發射台

一句話題意:長度為 \(n\) 的環,每個點染色,有 \(m\) 種顏色,要求相鄰相對不能同色,求方案數對 \(998244353\) 取模的結果。(定義兩個點相對為去掉這兩個點后環能被分成相同大小的兩段)

\(n,m \le 10^9\)

官方題解 UOJ Round #16

對於長度為奇數的環,就是經典問題,矩陣快速冪或者直接通項公式即可。

對於長度為偶數的環,就有點復雜了。因為要考慮相對點之間的相互影響,不妨將它們捆在一塊,裝在一個狀態里考慮。然后,我們需要處理環的上半部分和下半部分的相互接觸問題,類比處理經典問題的思路,

我們設第一格的顏色為 \(A\),設第 \(n/2+1\) 格的顏色為 B,然后設個二元三進制狀態表示第 i 格和第 \(n/2+i\) 格的顏色是否為顏色 A 或顏色 B(\(1≤i≤n/2\))。

\(F[i][0..8]\) 表示推到第 \(i\) 格的所有二元三進制狀態的合法方案數,然后遞推一波即可。

——UOJ Round #16 題解

這個討論有點變態,,,代碼就咕了(

彩色圓環

清橙A1202 bzoj2201 bsoj4074

試題來源

  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,顯然有

\[f[i]=\sum_{0 \le j < i} f[j]*(i-j)*P[i-j]*(M-1) \]

其中\(P[i]\)表示連續選\(i\)個相同一種顏色的概率

\[P[i] = M^{-i}\\ \]

\((M-1)\)代表當前顏色塊的顏色要與前塊不同

那么現在用圓環染色的思路來試着寫環的dp式

正如解決原始版本的方式,我們拆環為鏈,並假設已經欽定了第\(0\)位的顏色。我們設\(f[i][0/1]\)表示考慮前\(i\)位,且要求第\(i\)位(所屬塊)顏色是否(\(0/1\))與第\(0\)位顏色相同,這時的期望美觀度。可得轉移方程:

\[f[i][0] = \sum_{0 \le j < i} f[j][0]*(i-j)*P[i-j]*(m-2) + f[j][1]*(i-j)*P[i-j]*(m-1)\\ f[i][1] = \sum_{0 \le j < i} f[j][0]*(i-j)*P[i-j] \]

考慮如何求答案。由於無法直接獲取首尾相接顏色塊長度,考慮將它單獨拎出來計算。枚舉首尾相接顏色塊兩端加起來的總長度\(x\),則總共有\(x\)種分割首尾的方案,每種方案有\(M\)個顏色可以選擇(因為欽定),每個方案貢獻為\(x\),剩下的部分就可以用\(f\)來表示了。(想想欽定第\(0\)位而不是第\(1\)位的目的)

\(x=N\)時要特判,於是答案如下

\[Ans = P[N]*N*M + \sum_{1 \le x < N} x*x*P[x]*M*f[n-x][0] \]

\(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM