「學習筆記」遞推 & 遞歸

引入

假設我們想計算 \(f(x) = x!\)。除了簡單的 for 循環,我們也可以使用遞歸

遞歸是什麼意思呢?我們可以把 \(f(x)\)\(f(x – 1)\) 表示,即 \(f(x) = x \times f(x – 1)\)。這樣,我們就可以不斷地遞歸下去。

但是,這是永遠不會停止的。我們需要設立一個邊界條件,\(f(0) = 1\)。這樣,我們就可以寫出程式碼了。

int f(int x) {return x ? x * f(x - 1) : 1;}

實際上,遞歸有兩大要點:

  1. 自己調用自己;
  2. 回溯時還原現場。

如果你看到這裡,你或許已經懂了遞歸。那麼,遞推是什麼呢?把上面的計算階乘的遞歸改成遞推就是這樣的:

f[0] = 1;
for (int i = 1; i <= n; i++)
    f[i] = f[i - 1] * i;

一個有趣的例子:什麼是遞歸?

「例題」數樓梯

原題目鏈接:Link

我們用遞推的視角思考一下。假設我們要走到第 \(i\) 階台階,我們應該怎樣走?

記走第 \(i\) 階台階的走法為 \(f_i\),則顯然,我們可以從第 \(i – 1\) 階台階上一階台階到第 \(i\) 階台階,也可以從第 \(i – 2\) 階台階上二階台階到第 \(i\) 階台階。根據加法原理,有 \(f_i = f_{i – 1} + f_{i – 2}\)

遞推邊界可以根據題意得到。一階台階有 \(1\) 種走法,二階台階有 \(2\) 種走法,即 \(f_1 = 1, f_2 = 2\)

// Python Version
n = int(input())
a, b = 1, 2

if n < 3:
    print(n)

for i in range(n - 2):
    c = a + b
    a, b = b, c

if n >= 3:
    print(c)
# 可以使用滾動數組來優化,這裡 a 相當於 f[i - 2],b 相當於 f[i - 1],c 相當於 f[i]
# 注意特判
// C++ Version
#include <bits/stdc++.h>
using namespace std;

int n;
long long f[5005] = {0, 1, 2};
int main() {
    cin >> n;
    for (int i = 3; i <= n; i++)
    	f[i] = f[i - 1] + f[i - 2];
    cout << f[n] << endl;
    return 0;
}

這裡 C++ 只能得到 \(60\) 分。這是因為我們得到的結果超出了 long long 的表示範圍,需要使用高精度。

五大遞推模型

Fibonacci 數列

Fibonacci 數列,一般譯為斐波那契數列。Fibonacci 數列的邊界為 \(f_1 = f_2 = 1\),遞推柿為 \(f_i = f_{i – 1} + f_{i – 2}\)。上面的數樓梯就是 Fibonacci 數列從第 \(2\) 項開始的數列。

Fibonacci 數列還有一種表現形式,就是可愛的小兔子。

「例題」兔子的數量

原題目鏈接:Link

可以輕鬆寫出程式碼。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;

int a[MAXN] = {0, 1, 1}, n;
int main() {
    cin >> n;
    for (int i = 3; i <= n; i++)
    	a[i] = a[i - 1] + a[i - 2];
    cout << a[n] << endl;
    return 0;
}

Fibonacci 數列還有一個通項公式:\(f_n = \frac1{\sqrt5}[(\frac{1 + \sqrt5}{2}) ^ n – (\frac{1 – \sqrt5}{2}) ^ n]\)

Catalan 數

Catalan 數的實際應用很多。這裡我們來通過一道 NOIP 的題目來引出 Catalan 數。

「例題」棧

原題目鏈接:Link

我們可以設 \(i\) 個小球時,出管方式為 \(h_i\)。我們於是可以按最後一個出管的小球來討論。假設最後一個出管的為第 \(k\) 個小球,那麼比 \(k\) 早入管的小球為 \(k – 1\) 個,比 \(k\) 晚入管的小球為 \(n – k\) 個。這些小球全部出管之後,第 \(k\) 個小球才出管。根據乘法原理,最後一個出管的小球為第 \(k\) 個時,取法總數為 \(h_{k – 1} \times h_{n – k}\)。而 \(k\) 可以取 \(1\)\(n\)。所以總共的取法總數就是 \(h_n = \sum_{i = 1} ^ n h_{i – 1} \times h_{n – i}\)。這就是 Catalan 數的遞推柿了。遞推邊界顯然:\(f_0 = f_1 = 1\)

#include <bits/stdc++.h>
using namespace std;

int n, h[20] = {1, 1};
int main() {
    cin >> n;
    for (int i = 2; i <= n; i++)
    	for (int j = 1; j <= i; j++)
    		h[i] += h[j - 1] * h[i - j];
    cout << h[n] << endl;
    return 0;
}

Catalan 數列的應用:

  1. 把一個凸 \(n\) 邊形劃分為數個三角形的方案數(不相交)。
  2. 一個棧的進棧序列為 \(1, 2, 3, \dots, n\),不同的出棧序列數。
  3. \(n\) 個節點可以構造多少個不同的二叉樹的方案數。
  4. 在圓上選擇 \(2 \times n\) 個點,把這些點成對連接起來使得所得到的 \(n\) 條線段不相交的方法數。
  5. 從點 \((0, 0)\) 到點 \((n, n)\),不穿越但可以碰到對角線的最短走法數。

除了遞推,Catalan 數 \(h_n\) 還可以通過通項公式得到:\(h_n = \frac{C_{2n} ^ n}{n + 1} = C_{2n} ^ n – C_{2n} ^ {n – 1}\)

Hanio 塔

Hanio 塔是一個非常經典的遞推(遞歸)問題。或許很多人在感受 C++ 的強大時,教練展示的第一個程式就是 Hanio 塔。

「例題」Hanio 塔問題

原題目鏈接:Link

我們設 \(h_i\) 為移動 \(i\) 個盤子所需要的次數。顯然地,我們可以先把上面的 \(i – 1\) 個盤子移動到 B 柱上,再把第 \(i\) 的盤子移到 C 柱上,最後把 B 盤上的 \(i – 1\) 個盤子移到 C 柱上。那麼,遞推柿就出來了:\(h_i = 2 \times h_{i – 1} + 1\),邊界 \(h_1 = 1\)。Hanio 塔同樣有通項公式 \(h_n = 2 ^ n – 1\)

# Python Version
n = int(input())
# print(2 ** n - 1)
# 這樣可以直接輸出
a = 1
for i in range(n - 1):
    a = a * 2 + 1
print(a)
// C++ version
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 65;

long long a[MAXN] = {0, 1};
int n;
int main() {
	cin >> n;
    for (int i = 2; i <= n; i++)
    	a[i] = a[i - 1] << 1 | 1;
    cout << a[n] << endl;
    return 0;
}

第二類 Stirling 數

「例題」合理放球

原題目鏈接:Link

我們可以設 \(S_{n, m}\)\(n\) 個不相同的球放入 \(m\) 個相同的盒子(不為空)里的放法總數。

思考對於第 \(n\) 個球的放法。我們可以把這第 \(n\) 個球單獨放一個盒子,那麼前面的 \(n – 1\) 個球就要擠占 \(m – 1\) 個盒子,方案數為 \(S_{n – 1, m – 1}\);也可以把這第 \(n\) 個球與其他的球共佔一個盒子,那麼前面的 \(n – 1\) 個球就要擠占 \(m\) 個盒子(第 \(n\) 個球沒有額外佔用盒子),那麼方案數為 \((n – 1) \times S_{n – 1, m}\)。由加法原理知,\(S_{n, m} = S_{n – 1, m – 1} + (n – 1) \times S_{n – 1, m}\)

遞推邊界:\(S_{n, n} = S_{n, 1} = S_{1, m} = 1\)

#include <bits/stdc++.h>
using namespace std;

int n, m;

long long S(int n, int m) {
    if (n == 1 || m == 1 || n == m) return 1;
    return S(n - 1, m - 1) + m * S(n - 1, m);
}
int main() {
    scanf("%d %d", &n, &m);
    printf("%lld\n", S(n, m));
    return 0;
}

分割平面問題

這類問題比較多變,有時很困難,重點是要去找到規律(廢話)。

「例題」分割平面

題意簡述:設有 \(n\) 條封閉曲線畫在平面上,而任何兩條封閉曲線恰好相交於兩點,且任何三條封閉曲線不相交於同一點,問這些封閉曲線把平面分割成的區域個數。

分割平面

數據範圍:\(1 \leq n \leq 46314\)

觀察圖片,可以發現,如果設 \(f_n\) 表示由 \(n\) 條封閉曲線時,題意中的區域個數,則有 \(f_n = f_{n – 1} + 2 \times (i – 1)\)\(f_1 = 2\)

證明也比較顯然。當畫第 \(n\) 條曲線時,會與每條之前畫的 \(n – 1\) 條曲線形成 \(2\) 個交點,多一個交點就多一個區域,由此可得。

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 46350;
int n;
long long a[MAXN] = {0, 2};
int main() {
    scanf("%d", &n);
    for (int i = 2; i <= n; i++)
    	a[i] = a[i - 1] + ((i - 1) << 1);
    printf("%lld\n", a[n]);
    return 0;
}

讓我們再來看一道題,加深印象。

「例題」分割平面之二

題意簡述:同一平面內有 \(n\) 條直線,已知其中 \(p\) 條直線相交於同一點,則這 \(n\) 條直線最多能將平面分割成多少個不同的區域?

數據範圍:\(2 \leq p \leq n \leq 500\)


與上一題類似地設出 \(f_n\)。畫圖容易發現 \(p\) 條直線相交於同一點會產生 \(2p\) 個區域,第 \(i\) 條直線會相交多產生 \(i\) 個區域,所以有 \(f_i = f_{i – 1} + i (i > p)\),邊界為 \(f_p = 2p\)

#include <bits/stdc++.h>
using namespace std;

int n, p;
int f[505];
int main() {
	cin >> n >> p;
	f[p] = p << 1;
	for (int i = p + 1; i <= n; i++)
		f[i] = f[i - 1] + i;
	cout << f[n] << endl;
	return 0;
}

記憶化

有些時候,使用遞歸會超時,原因是進行了大量的重複運算導致效率低下。而記憶化就可以提升效率。甚至於你可以用記憶化搜索來代替 DP。

「例題」Function

原題目鏈接:Link

我們可以按題意容易地寫出樸素程式碼:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
#define LL long long

LL a, b, c;
int w(LL a, LL b, LL c) {
    if (a <= 0 || b <= 0 || c <= 0) return 1;
    if (a > 20 || b > 20 || c > 20) return w(20, 20, 20);
    if (a < b && b < c) return w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c);
    return w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);
    }
int main() {
    while (~scanf("%lld %lld %lld", &a, &b, &c)) {
        if (a == -1 && b == -1 && c == -1) break;
        printf("w(%lld, %lld, %lld) = %d\n", a, b, c, w(a, b, c));
    }
    return 0;
}

試一試樣例,哇,只用了 \(22\) 毫秒就過了!

然後交上去,快樂 TLE。

這是為什麼呢?如果我們把 \(w\) 函數加上 cout << a << " " << b << " " << c << endl;,再運行,你會發現輸出了很多行,但其中有許多是重複的。

這啟發我們,可以用一個數組 \(f\) 來存儲 \(w_{a, b, c}\),如果已經得到過這個值就可以直接返回,避免無效運算。這就是記憶化。在這道題中,我們使用 \(-1\) 表示沒有算過。具體地說,如果 \(f_{a, b, c} = -1\),那麼沒有計算過,我們就直接計算,並把結果存儲在 \(f_{a, b, c}\) 中;否則算過就直接 return \(f_{a, b, c}\) 即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
#define LL long long
    
int f[MAXN][MAXN][MAXN];
LL a, b, c;
int w(LL a, LL b, LL c) {
    if (a <= 0 || b <= 0 || c <= 0) return 1;
    if (a > 20 || b > 20 || c > 20) return w(20, 20, 20);
    if (a < b && b < c) {
        if (f[a][b][c] != -1) return f[a][b][c];
        // 如果已經得到值,直接返回
        return f[a][b][c] = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c);
        // 否則計算這個值,並存儲起來,下一次就不會繼續遞歸了
    }
    if (f[a][b][c] != -1) return f[a][b][c];
    // 同上
    return f[a][b][c] = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);
}
int main() {
    memset(f, -1, sizeof(f)); // 全部初始化為 -1 表示沒有得到值
    while (~scanf("%lld %lld %lld", &a, &b, &c)) {
        if (a == -1 && b == -1 && c == -1) break;
        printf("w(%lld, %lld, %lld) = %d\n", a, b, c, w(a, b, c));
    }
    return 0;
}

怎麼樣,你學廢了嗎?

讓我們一起來看一道 IOI 的題目(好吧是 \(1994\) 年的)。

「例題」數字三角形 Number Triangles

原題目鏈接:Link

假設你不會遞推。我們可以設 \(f_{i, j}\) 表示從 \((i, j)\) 走到最底下的最大的和。那麼,顯然有 \(f_{i, j} = w_{i, j} + \max(f_{i + 1, j}, f_{i + 1, j + 1})\)。寫成程式碼就是這樣的:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3 + 5;

int n;
int w[MAXN][MAXN];
    
int f(int i, int j) {
    if (i == n) return w[i][j]; // 已經不能再走了,直接返回 w[i][j]
    return w[i][j] + max(f(i + 1, j), f(i + 1, j + 1));
}
int main() {
	cin >> n;
    for (int i = 1; i <= n; i++)
    	for (int j = 1; j <= i; j++)
    		cin >> w[i][j];
    cout << f(1, 1) << endl;
    return 0;
}

但這還是會超時,怎麼辦呢?

沒錯,我們可以使用記憶化。類似地,我們用 \(-1\) 表示沒搜過 \((i, j)\),如果不等於 \(-1\) 直接 return 即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e3 + 5;

int n;
int w[MAXN][MAXN], ans[MAXN][MAXN];
    
int f(int i, int j) {
    if (i == n) return w[i][j];
    if (ans[i][j] != -1) return ans[i][j];
    return ans[i][j] = w[i][j] + max(f(i + 1, j), f(i + 1, j + 1));
}
int main() {
    memset(ans, -1, sizeof(ans));
    cin >> n;
    for (int i = 1; i <= n; i++)
    	for (int j = 1; j <= i; j++)
    		cin >> w[i][j];
    cout << f(1, 1) << endl;
    return 0;
}

其實,這已經有點 DP 的意味了。

雜題

「例題」鋪磚

題意簡述:有一個 \(2 \times n\) 的走道,現在用 \(1 \times 2\)\(2 \times 2\) 的磚去鋪滿。問有多少種不同的鋪法?

數據範圍:多組數據,\(0 \leq n \leq 250\)

先設 \(f_n\) 表示鋪成 \(2 \times n\) 的走道的方案數。\(2 \times n\) 的走道是怎麼鋪成的?可以由一個 \(1 \times 2\) 的磚豎著鋪在最右邊,問題就轉化為了鋪剩下的 \(2 \times (n – 1)\) 的走道,方案數即為 \(f_{n – 1}\);也可以由兩個 \(1 \times 2\) 的磚橫著鋪在最右邊,問題就轉化為了鋪剩下的 \(2 \times (n – 2)\) 的走道,方案數即為 \(f_{n – 2}\);還可以直接由一個 \(2 \times 2\) 的磚鋪在最右邊,問題就轉化為了鋪剩下的 \(2 \times (n – 2)\) 的走道,方案數也為 \(f_{n – 2}\)。綜上,我們可以得到遞推柿 \(f_i = f_{i – 1} + f_{i – 2} \times 2\)。邊界顯然為 \(f_1 = 1, f_2 = 3\)

注意這裡是多測,所以可以提前處理出 \(f_3\)\(f_{250}\),一一回答即可。需要使用高精度。

「例題」統計奇數和偶數個 3

原題目鏈接:Link

這裡有兩個維度:位數和奇偶。我們就可以把這兩個維度設出來:設 \(f_{n, 0}\) 表示 \(n\) 位正整數中有多少個數含偶數個數字 \(3\)\(f_{n, 1}\) 表示 \(n\) 位正整數中有多少數含個奇數個數字 \(3\)

\(n\) 位正整數中,含偶數個數字 \(3\) 的數是怎麼來的呢?有兩種情況:

  1. 前面 \(n – 1\) 位含奇數個數字 \(3\),第 \(n\) 位是 \(3\)。個數為 \(f_{n – 1, 1}\)
  2. 前面 \(n – 1\) 位含偶數個數字 \(3\),第 \(n\) 位不是 \(3\)(是 \(0, 1, 2, 4, 5, 6, 7, 8, 9\))。個數為 \(f_{n – 1, 0} \times 8\)

所以可以得到 \(f_{n, 0} = f_{n – 1, 1} + f_{n – 1, 0} \times 8\)。同理有 \(f_{n, 1} = f_{n – 1, 0} + f_{n – 1, 1} \times 8\)

「例題」螺旋矩陣

原題目鏈接:Link

思考如何遞歸,發現矩陣外面那一圈兒都是可以直接算出來的(知道 \(n\))。接著觀察除去外面那一圈兒後剩下的矩陣,發現只要減去 \(4n – 4\) 後就是一個新的矩陣。

拿這個圖舉個例子:

可愛的矩陣

\(1 \sim 12\) 可以直接算出,而剩下的 \(13, 14, 15, 16\) 就相當於 \(12 + 1, 12 + 2, 12 + 3, 12 + 4\)

程式碼如下:

#include <cstdio>

int n, i, j;

int f(int m, int x, int y) {
	if (x == 1) return y;
	if (x == m) return 3 * m - y - 1;
	if (y == 1) return 4 * m - x - 2;
	if (y == m) return m + x - 1; // 邊界情況,可以自己推導
	return f(m - 2, x - 1, y - 1) + 4 * m - 4;
    // 規模縮小,記得加上 4 * m - 4
}
int main() {
	scanf("%d %d %d", &n, &i, &j);
	printf("%d\n", f(n, i, j));
	return 0;
}

「例題」無限的路

原題目鏈接:Link

給出兩個點 \((x_1, y_1), (x_2, y_2)\),要求連接的折線長度,為了方便計算,我們可以統一成到點 \((0, 0)\) 的折線長度,兩者相減就會抵消,剩下我們要求的答案。

現在,問題就轉化為給出點 \((x, y)\),求它到點 \((0, 0)\) 的折線長度。

美妙的折線

我們可以發現(結合圖食用更佳):

  • \(x = 0\) 時,我們可以從 \((0, y)\) 走到 \((y – 1, 0)\),再加上點 \((y – 1, 0)\) 到點 \((0, 0)\) 的折線長度。
  • \(y = 0\) 時,我們可以從 \((x, 0)\) 走到 \((0, x)\),再加上點 \((0, x)\) 到點 \((0, 0)\) 的折線長度。
  • \(x \not= 0\)\(y \not= 0\) 時,我們可以從 \((x, y)\) 走到 \((0, x + y)\),再加上點 \((0, x + y)\) 到點 \((0, 0)\) 的折線長度。

然後用上我們小學二年級就學過的兩點間直線距離 \(\sqrt{(x_1 – x_2) ^ 2 + (y_1 – y_2) ^ 2}\) 就行了。邊界顯然是 \((0, 0)\)

由於這裡是 double 類型,不好直接記憶化,所以我們可以開一個 bool 數組打標記。程式碼如下:

#include <cstdio>
#include <cmath>

double ans[205][205];
bool flag[205][205];

double f(int x1, int my1, int x2, int y2) {
	return sqrt(pow(x1 - x2, 2) + pow(my1 - y2, 2));
} // 兩點間直線距離

int n, x1, x2, my1, y2; // y1 是關鍵字,隨便加個字母

double g(int x, int y) {
	if (!x && !y) return 0; // (0, 0) 邊界
	if (flag[x][y]) return ans[x][y]; // 已經搜過
	flag[x][y] = true;
	if (!x) return ans[x][y] = g(y - 1, 0) + f(x, y, y - 1, 0);
	if (!y) return ans[x][y] = g(0, x) + f(x, y, 0, x);
	return ans[x][y] = g(0, x + y) + f(x, y, 0, x + y); // 三種情況
}
int main() {
	scanf("%d", &n);
	while (n--) {
		scanf("%d %d %d %d", &x1, &my1, &x2, &y2);
		printf("%.3lf\n", fabs(g(x1, my1) - g(x2, y2))); // 記得加絕對值
	}
	return 0;
}

「例題」最大奇約數

原題目鏈接:Link

每一個數都能表示成 \(x = 2 ^ k \times a\) 的形式(\(2 \nmid a\)),這道題的 \(f(x)\) 相當於求出 \(a\)。答案為 \(\sum _ {i = 1} ^ n f(i)\)

如果老老實實地算 \(\sum _ {i = 1} ^ N f(i)\) 會超時,加上記憶化又會 MLE,這啟發我們令一個函數 \(g(n) = \sum _ {i = 1} ^ n f(i)\)

然後我們推公式。

  • \(2 \nmid n\),顯然 \(g(n) = n + g(n – 1)\)
  • \(2 \mid n\),發現 \(f(i)\) 可以根據奇偶性分成兩類,\(f(2k) = f(k)\),而 \(f(2k + 1) = 2k + 1\)。從而也可以把 \(\sum _ {i = 1} ^ n f(i)\) 分成兩類,一類是 \(f(1) + f(3) + f(5) + \dots + f(n – 1) = 1 + 3 + 5 + \dots + (n – 1)\),一類是 \(f(2) + f(4) + f(6) + \dots + f(n) = f(1) + f(2) + f(3) + \dots + f(\frac{n}{2})\);前者可以等差數列求和暴算,而後者發現就等於 \(f(\frac{n}2)\),直接遞歸即可。
#include <cstdio>

int n;

long long f(int x) {
	if (x == 1) return 1; // 邊界條件
	if (x & 1) return f(x - 1) + x;
	return f(x >> 1) + (long long)x * x / 4;
    // 因為 x * x 可能會溢出,所以先轉換成 long long 再乘
}
int main() {
	scanf("%d", &n);
	printf("%lld\n", f(n));
	return 0;
}

「例題」Strange Towers of Hanoi

原題目鏈接:Link

題目要求我們求 Hanoi 塔的移動步數。這個簡單,秒了!——真的這麼簡單嗎?啊,是 \(4\) 座塔!(一般的 Hanoi 塔是 \(3\) 座。)

不過,題目中有這樣一段話:

此演算法適用於 \(n \leq 12\):塔 A 上的 \(k\)\(k \geq 1\))個盤子是固定的,其餘 \(n – k\) 個盤子使用四塔演算法從 A 移到 B。然後用三塔演算法把 A 上的 \(k\) 個盤子移到 D,最後使用四塔演算法將 B 上的 \(n – k\) 個盤子再次移動到 D。

可以設 \(n\) 個盤子時用三塔需要 \(g_n\) 步,我們有這樣的方案:把 A 最上面的 \(n – 1\) 個盤子移到 B,A 上的第 \(n\) 個盤子移到 C,然後把 B 上的 \(n – 1\) 個盤子移到 C。從而有 \(g_n = 2 \times g_{n – 1} + 1\)

再設 \(n\) 個盤子時用四塔需要 \(f_n\) 步。因為題目已經告訴了我們公式,遍歷 \(k\)\(\min\) 即可,不用白不用哇:

\[f_n = \min_{1 \leq k < n} 2 \times f_{n – k} + g_k
\]

「例題」棋子移動

原題目鏈接:Link

首先,我們發現 \(n = 4\) 時僅有這一種固定移動方法:

4,5-->9,10

8,9-->4,5

2,3-->8,9

7,8-->2,3

1,2-->7,8

這顯然就是遞歸邊界。那當 \(n > 4\) 時呢(下面設 \(n = 5\))?

〇〇〇〇〇●●●●●

容易想到,先把中間的移到最右邊。

〇〇〇〇[ ][ ]●●●●〇●

然後把最右邊的兩個黑棋子移到空位里去。

〇〇〇〇●●●●[ ][ ]〇●

發現左邊縮小規模成了 \(n – 1 = 4\) 的情況,繼續遞歸即可。

程式碼十分簡單:

#include <cstdio> 
#include <iostream>

int n;

void print(int n) {
	if (n == 4) {
		puts("4,5-->9,10\n\n8,9-->4,5\n\n2,3-->8,9\n\n7,8-->2,3\n\n1,2-->7,8");
		return;
	}
	std::cout << n << "," << n + 1 << "-->" << (n << 1 | 1) << "," << (n * 2 + 2) << std::endl << std::endl;
	std::cout << 2 * n - 1 << "," << (n << 1) << "-->" << n << "," << n + 1 << std::endl << std::endl;
	print(n - 1);
}
int main() {
	std::cin >> n;
	print(n);
	return 0;
}

By the way,建議大家輸出複雜格式時還是用 printf,cout 要打死了。。。

「例題」分形圖1

原題目鏈接:Link

題目要求我們輸出一個奇怪的 X 形分形圖。觀察發現,當規模為 \(n\) 時,其實就是由 \(5\) 個規模為 \(n – 1\) 的 X 形組合而成。一般對於這種分形圖,我們都會遞歸規模和坐標。

程式碼非常簡單:

#include <bits/stdc++.h>
using namespace std;

int n;
char ch[735][735];
void print(int n, int x, int y) { // n 是規模,(x, y) 是圖形左上角坐標
	if (n == 1) {
		ch[x][y] = 'X';
		return;
	} // 邊界情況
	int t = pow(3, n - 2); // n - 1 規模圖形的長(寬)
	print(n - 1, x, y); // 列印左上
	print(n - 1, x + (t << 1), y); // 列印左下
	print(n - 1, x + t, y + t); // 列印中間的
	print(n - 1, x, y + (t << 1)); // 列印右上
	print(n - 1, x + (t << 1), y + (t << 1)); // 列印右下
}

int main() {
	while (cin >> n) {
		memset(ch, 0, sizeof(ch)); // 記得清零
		print(n, 1, 1);
		int len = pow(3, n - 1);
		for (int i = 1; i <= len; i++) {
			for (int j = 1; j <= len; j++)
				if (ch[i][j]) putchar(ch[i][j]);
				else putchar(' ');
				// 沒填充到的地方默認是 0,直接輸出的話,肉眼看不出與空格的差別,但是會 WA
				// 所以這裡特判一下
			putchar('\n');
		}
		putchar('-');
		putchar('\n');
	}
	return 0;
}

附上一個奇怪分形圖的程式碼:

#include <iostream>
using namespace std;

int n;
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++)
			putchar((i & j) ? '#' : ' ');
		putchar('\n');
	}
	return 0;
}

可以試試用遞歸實現。