【題目解析】藍橋杯23國賽C++中高階組 - 鬥魚養殖場
題目連結跳轉:點選跳轉
前置知識:
- 瞭解過基本的動態規劃。
- 熟練掌握二進位制的位運算。
題解思路
這是一道典型的狀壓動態規劃問題。設 \(dp_{i, j}\) 表示遍歷到第 \(i\) 行的時候,當前行以 \(j_{(base2)}\) 的形式排列烏龜可以構成的方案數。
對於每一行的方案,我們可以用一個二進位制來表示。例如二進位制數字 \(10100\),表示有一個橫向長度為 \(5\) 的場地中,第 \(1, 3\) 號位置分別放置了一隻小烏龜。因此,每一種擺放狀態都可以用一個二進位制數字來表示。我們也可以透過遍歷的方式來遍歷出二進位制的每一種擺放狀態。
首先,我們預處理出橫排所有放置烏龜的合法情況。根據題意,兩個烏龜不能相鄰放置,因此在二進位制中,不能有兩個 \(1\) 相鄰。如何預處理出這種情況呢?我們可以使用位運算的方法:
如果存在一個二進位制數字有兩個 \(1\) 相鄰,那麼如果我們對這個數字 \(x\) 進行位運算操作 (x << 1) & x
的結果或 (x >> 1) & x
的結果必定大於等於 \(1\)。我們透過把這種情況排除在外。同時,我們還需要注意有些格子中不能放置烏龜。這一步也可以透過二進位制的方法預處理掉,如果網箱在第 \(i\) 一個格子中不能放置烏龜,那麼在列舉所有方案數的時候直接忽略掉第 \(i\) 位為 \(1\) 的情況即可。
接下來如何保證上下兩行的烏龜不衝突?假如上一行的擺放狀態是 \(y\),當前行的擺放狀態為 \(j\),如果 i & j
的結果大於等於 \(1\),也可以證明有兩個數字 \(1\) 在同一位置上。因此我們也需要把這種情況排除在外。
綜上所述,我們可以得出狀態轉移方程:\(dp_{i, j} = dp_{i, j} + dp_{i-1, k}\)。其中,\(j\) 和 \(k\) 表示所有橫排合法的方案。答案就是 \(\mathtt{ANS} = \sum_{j=0}^{2^M-1}{dp_{N, j}}\)。
狀態的初始化也很簡單,另 \(dp_{0, 0} = 1\),表示一隻烏龜都不放有一種擺放方案。
時間複雜度
透過觀察上述程式碼,在列舉所有狀態和轉移狀態的時候有三層迴圈,分別是列舉當前行、列舉當前行的合法擺放情況以及列舉上一行的擺放情況。因此總時間複雜度約為 \(O(n \times 2^M \times 2^M) = O(n \times 2^{M^2}) = O(n \times 4^M)\)。但由於合法的擺放數量遠遠少於 \(2^M\),因此實際情況下程式執行的速度會快許多。
程式碼實現
本題的程式碼實現如下。在輸出的時候需要減一,因為不放置也是一種合法情況,根據題目要求需要把這一合法情況排除。
#include <iostream>
using namespace std;
const int MOD = 1e9+7;
int n, m, ans;
int arr[505][505];
// 所有橫排合法的情況。
int terrain[505];
int ok[1050], cnt;
int dp[505][1050];
int main(){
cin >> n >> m;
for (int i=1; i<=n; i++){
for (int j=1; j<=m; j++){
cin >> arr[i][j];
}
}
// 預處理非法地形。
for (int i=1; i<=n; i++){
for (int j=1; j<=m; j++){
terrain[i] = (terrain[i] << 1) + !arr[i][j];
}
}
// 預處理出所有橫排的合法情況。
for (int i=0; i<(1<<m); i++){
if (((i<<1)|(i>>1)) & i) continue;
ok[++cnt] = i;
}
dp[0][1] = 1;
// 列舉。
for (int i=1; i<=n; i++){
for (int s1=1; s1<=cnt; s1++){ // 列舉當前行。
if (ok[s1] & terrain[i]) continue;
for (int s2=1; s2<=cnt; s2++){ // 列舉上一行。
if (ok[s2] & terrain[i-1]) continue;
if (ok[s1] & ok[s2]) continue;
dp[i][s1] = (dp[i][s1] + dp[i-1][s2]) % MOD;
}
}
}
// 統計答案。
int ans = 0;
for (int i=1; i<=cnt; i++)
ans = (ans + dp[n][i]) % MOD;
cout << ans - 1 << endl;
return 0;
}
本題的 Python 程式碼如下,Python 可以透過本題的所有測試點:
MOD = int(1e9 + 7)
n, m, ans = 0, 0, 0
arr = [[0] * 505 for _ in range(505)]
terrain = [0] * 505
ok = [0] * 1050
dp = [[0] * 1050 for _ in range(505)]
cnt = 0
def main():
global n, m, cnt, ans
# 輸入 n 和 m
n, m = map(int, input().split())
# 輸入 arr 陣列
for i in range(1, n + 1):
arr[i][1:m + 1] = map(int, input().split())
# 預處理非法地形
for i in range(1, n + 1):
for j in range(1, m + 1):
terrain[i] = (terrain[i] << 1) + (1 - arr[i][j])
# 預處理出所有橫排的合法情況
for i in range(1 << m):
if ((i << 1) | (i >> 1)) & i:
continue
cnt += 1
ok[cnt] = i
dp[0][1] = 1
# 列舉
for i in range(1, n + 1):
for s1 in range(1, cnt + 1): # 列舉當前行
if ok[s1] & terrain[i]:
continue
for s2 in range(1, cnt + 1): # 列舉上一行
if ok[s2] & terrain[i - 1]:
continue
if ok[s1] & ok[s2]:
continue
dp[i][s1] = (dp[i][s1] + dp[i - 1][s2]) % MOD
# 統計答案
ans = 0
for i in range(1, cnt + 1):
ans = (ans + dp[n][i]) % MOD
print(ans - 1)
if __name__ == "__main__":
main()
再提供一個暴力解法用於對拍:
#include <iostream>
using namespace std;
const int MOD = 1e9+7;
int n, m, ans;
int arr[505][505];
int dx[] = {0, 1, -1, 0};
int dy[] = {1, 0, 0, -1};
// 深度優先搜尋 Brute Force
void dfs(int x, int y){
if (x > n) {
ans += 1;
ans %= MOD;
return ;
}
if (y > m){
dfs(x+1, 1);
return ;
}
if (arr[x][y] == 0){
dfs(x, y+1);
return ;
}
// 不放魚
dfs(x, y+1);
// 放魚
for (int i=0; i<4; i++){
int cx = x + dx[i];
int cy = y + dy[i];
if (cx < 1 || cy < 1 || cx > n || cy > m) continue;
if (arr[cx][cy] == 2) return ;
}
arr[x][y] = 2;
dfs(x, y+1);
arr[x][y] = 1;
return ;
}
int main(){
cin >> n >> m;
for (int i=1; i<=n; i++){
for (int j=1; j<=m; j++){
cin >> arr[i][j];
}
}
// dfs 暴力
dfs(1, 1);
cout << ans-1 << endl;
return 0;
}