「 洛谷 」P2768 珍珠項鍊

Rabbit_Mua發表於2020-12-12

小兔的話

推薦 小兔的CSDN


珍珠項鍊

題目限制

  • 記憶體限制:125.00MB
  • 時間限制:1.00s
  • 標準輸入輸出

題目知識點

  • 動態規劃 \(dp\)
  • 矩陣
    • 矩陣乘法
    • 矩陣加速
    • 矩陣快速冪

題目來源

「 洛谷 」P2768 珍珠項鍊


為了方便大家閱讀通暢,題目可能略有改動,保證不會造成影響

題目

題目背景

\(L\) 通過泥萌的幫助,成功解決了牛欄的修建問題
奶牛們覺得主人非常厲害,就再也不偷懶了:母牛們奮力擠奶、生娃
\(L\) 也因此成為了一代富豪

但是一直困擾 小\(L\) 的就是單身問題
\(L\) 經過長久的尋覓,小\(L\) 終於找到了一個心儀的漂亮妹子
於是,小\(L\) 打算在 \(5.20\) 那天給妹子一個驚喜

題目描述

\(L\) 決定用 \(K\) 種珍珠為妹子做一串舉世無雙的珍珠垂飾
珍珠垂飾是由珍珠連線而成的,其長度可以認為就是珍珠垂飾上珍珠的個數

\(L\) 現在腰纏萬貫,每種珍珠他都擁有 \(N\)
根據將珍珠垂飾開啟後珍珠不同的排列順序可以區別出不同種類的項鍊
現在,小\(L\) 好奇自己可以組成多少種長度為 \(1\)\(N\) 且不同的珍珠垂飾
當然,為顯富有,每串珍珠垂飾都要必須由 \(K\) 種珍珠連線而成
由於答案可能太大,只需要取模 1234567891

這一定難不倒聰明的你吧
如果你能幫 小\(L\) 解決這個問題,他會把最後的資產分給你一半哦

格式

輸入格式

第一行輸入一個整數 \(T\) ,表示測試資料的個數

  • 對於每組資料:
  • 輸入佔一行,包含兩個整數 \(N\)\(K\),用一個空格隔開

輸出格式

  • 對於每組資料
  • 輸出佔一行,包含一個整數,表示項鍊的種類數

樣例

樣例輸入

2
2 1
3 2

樣例輸出

2
8

提示

時間限制

對於 \(70\% \sim 100\%\) 的資料:時間限制 \(10ms\)

資料範圍

對於 \(40\%\) 的資料:\(1<= N<= 1e5\)\(0 \leq K \leq 30\)
對於 \(100\%\) 的資料:\(T \leq 10\)\(1 \leq N \leq 1e9\)\(0 \leq K \leq 30\)


思路

題目所說的 珍珠項鍊長度為 \(i\) 的種類數 其實就是 用不同的方式連線出長度為 \(i\) 的珍珠項鍊方案數
我們可以先思考 \(dp\)\(dp[i][j]\) 表示 \(j\) 種珍珠 連線出 長度為 \(i\) 的珍珠項鍊的方案數

  • 很多讀者在這裡可能會有一個疑問:\(dp\) 的狀態 \(dp[i][j]\),似乎沒有考慮每種珍珠的數量
  • 因為每種珍珠的個數是 \(N\) 顆,所需要求的最大長度也是 \(N\);也就是說,可以只用 \(1\) 種珍珠連線出所有的項鍊,每種珍珠的數量都是足夠的;因此,珍珠的數量是不會造成影響的,可以忽略不計

我們可以進一步推出 \(dp\) 的狀態轉移方程:\(dp[i][j] = dp[i - 1][j - 1] * (K - (j - 1)) + dp[i - 1][j] * j\)

  • \(dp[i - 1][j - 1] * (k - (j - 1))\):當 \(i\) 顆珍珠前面 \(i - 1\) 顆用過的珍珠種類不同 的時候,所連線出長度為 \(i\) 的珍珠的方案數
    • \(dp[i - 1][j - 1]\):用 \(j - 1\) 種珍珠 連線出 長度為 \(i - 1\) 的項鍊 的方案數
    • \(k - (j - 1)\)前面的 \(i - 1\) 顆珍珠 已經用了 \(j - 1\) 種珍珠,而 \(i\) 顆使用沒有用過的珍珠,就會有 剩下的珍珠種數 種情況,即 \(k - (j - 1)\)
  • \(dp[i - 1][j] * j\):當 \(i\) 顆的珍珠的種類 是之前用過的時候,所連線出來的方案數
    • \(dp[i - 1][j]\)\(j\) 種珍珠 連線出 長度為 \(i - 1\) 的項鍊 的方案數
    • \(j\):前面 \(i - 1\) 顆珍珠用了 \(j\) 種,\(i\) 顆珍珠 還是使用 \(j\) 種中的某一種,就有 \(j\) 種情況

最後的答案就是 \(dp[1][K] + dp[2][K] + dp[3][K] + ··· + dp[N][K] = \sum_{i = 1}^{N} dp[i][K]\)


分析

如果這道題的資料較小的話,用 \(dp\) 就可以 \(AC\) 了,可惜 \(N\) 的範圍太大了
這時候我們就要思考如何優化 \(dp\) 的時間複雜度了

\(dp\) 是在進行遞推轉移,不妨可以把 \(dp\) 放在矩陣裡求解
我們設 \(ans[i] = dp[1][K] + dp[2][K] + ··· + dp[i][K]\),我們所求的答案就是 \(ans[N]\)

我們可以構造一個 原始矩陣

\[\begin{bmatrix} & dp[1][1] & dp[1][2] & dp[1][3] & ··· & dp[1][K] & ans[0] & \\ \end{bmatrix} \]

為了狀態轉移,我們需要將它變成:

\[\begin{bmatrix} & dp[2][1] & dp[2][2] & dp[2][3] & ··· & dp[2][K] & ans[1] & \\ \end{bmatrix} \]

繼續進化成:

\[\begin{bmatrix} & dp[3][1] & dp[3][2] & dp[3][3] & ··· & dp[3][K] & ans[2] & \\ \end{bmatrix} \]

以此類推,直到求出 \(ans[N]\) 為止

構造的原始矩陣\(dp\) 的狀態轉移方程 中,我們可以推出 加速矩陣 (\(k\)\(k\) 列)

\[\begin{bmatrix} & 1 & k - 1 & 0 &0 & ··· & 0 & 0 & \\ & 0 & 2 & k - 2 &0 & ··· & 0 & 0 & \\ & 0 & 0 & 3 & k - 3 & ··· & 0 & 0 & \\ & ··· & ··· & ··· & ··· & ··· & 0 & 0 & \\ & ··· & ··· & ··· & ··· & ··· & 1 & 0 & \\ & 0 &0 &0 &0 &0 &k &1 & \\ & 0 &0 &0 &0 &0 &0 &1 & \\ \end{bmatrix} \]

剛開始是 \(ans[0]\),答案是 \(ans[N]\),所以要乘以 \(N\) 個加速矩陣,可是仍然要超時

這時候就需要矩陣快速冪了:根據矩陣乘法的結合律,先把 \(N\) 個加速矩陣乘起來,再用 原始矩陣 乘以 這個得到的矩陣,就可以得到最終的答案了
(注意:矩陣1 \(*\) 矩陣2 不一定等於 矩陣2 \(*\) 矩陣1,所以不能乘反了)


程式碼

#include <cstdio>

const long long MOD = 1234567891LL;
int T, N, K;

const int MAXK = 30;
struct Matrix
{
	long long Mat[MAXK + 5][MAXK + 5];
	int R, C;
	
	Matrix()
	{
		for (int i = 1; i <= MAXK + 1; i++)
			for (int j = 1; j <= MAXK + 1; j++)
				Mat[i][j] = 0LL;
	}
	
	void Read() const // 矩陣的輸入 
	{
		if (R < 1 || C < 1) return ;
		for (int i = 1; i <= R; i++)
			for (int j = 1; j <= C; j++)
				scanf("%lld", &Mat[i][j]);
	}
	
	void Write() const // 矩陣的輸出 
	{
		if (R < 1 || C < 1) return ;
		for (int i = 1; i <= R; i++) {
			for (int j = 1; j < C; i++)
				printf("%lld ", Mat[i][j]);
			printf("%lld\n", Mat[i][C]);
		}
	}
	
	Matrix operator * (const Matrix One) const // 過載矩陣的乘號 
	{
		Matrix Res;
		Res.R = R, Res.C = C;
		for (int i = 1; i <= Res.R; i++) 
			for (int j = 1; j <= Res.C; j++)
				for (int k = 1; k <= One.R; k++)
					Res.Mat[i][j] = (Res.Mat[i][j] + Mat[i][k] * One.Mat[k][j]) % MOD;
		return Res;
	}
}A, B;

Matrix Pow(Matrix One, long long k) // 矩陣快速冪 
{
	Matrix Res, cnt = One;
	Res.R = K + 1, Res.C = K + 1;
	for (int i = 1; i <= K + 1; i++)
		Res.Mat[i][i] = 1LL; // 單位矩陣 
	for (int i = k; i >= 1; i >>= 1)
	{
		if (i & 1) Res = Res * cnt;
		cnt = cnt * cnt;
	}
	return Res;
}

void Init(int k) // 初始化 原始矩陣 和 加速矩陣 
{
	A.R = 1, A.C = k + 1;
	A.Mat[1][1] = (long long)k;
	
	B.R = B.C = k + 1;
	B.Mat[1][1] = 1LL;
	for (int i = 2; i <= k; i++)
	{
		B.Mat[i][i] = (long long)i;
		B.Mat[i - 1][i] = (long long)k + 1LL - i;
	}
	B.Mat[k][k + 1] = B.Mat[k + 1][k + 1] = 1LL;
}

int main()
{
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d %d", &N, &K);
		Init(K);
		Matrix ans = A * Pow(B, N);
		printf("%lld\n", ans.Mat[1][K + 1]);
	}
	return 0;
}