在考慮枚舉所有可行解的同時,若變數與變數之間有高度的相關性,那麼枚舉的順序就會變得相當重要。若我們用了錯誤的順序進行枚舉,很可能會浪費許多時間考慮根本不符合題目規定的解。

例題 1:[CF161E] Polycarpus the Safecracker

Polycarpus 有 $t$ 個保險箱,每一個保險箱的密碼都是一個上面填了 $0$ 到 $9$ 之間數碼的方陣。由於 Polycarpus 實在太喜歡質數了,他把密碼設定成每一列都是質數。令他感到驚奇的是,每一組他設定的密碼都是對稱矩陣。事隔多年, Polycarpus 只記得每組密碼的第一列的數字 $p_i$,請你幫他算算,有多少種可能的密碼?除了第一列以外,其他列允許有前導 $0$ 的出現。第一列數字 $10\le p_i\le 99999$,總共有 $1\le t\le 30$ 組詢問。

因為 $p_i\le 99999$ 因此密碼鎖的大小至多只有 $5\times 5$。再加上對稱的關係,因此彼此不相關的格子只有對角線右上方的這 15 格:

但是,因為第一排已經固定了 $p_i$,所以剩下下面的 10 格。注意到,如果我們枚舉了標記數字 1 到 6 的地方,則每一排恰好空下一個對角線上的格子,而且彼此不相關。我們只要解決:有多少質數滿足 $\overline{aXbcd}$ 的形式,其中 $a,b,c,d$ 已知。而這一步可以預處理後 $O(1)$ 查詢。因此利用枚舉法的計算量約在 $10^6$,相當合理。

參考程式碼

// by tmt514
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// cnt[第k個位數被丟掉了][其他數字是多少] = 有幾個質數。
int cnt[5][100000];

void pre_compute() {
  // 先用篩法求質數。
  vector<int> sieved(99999, 0);
  for (int i = 2; i*i <= 99999; i++)
    for (int j = i*i; j <= 99999; j += i)
      sieved[j] = 1;
  // 對於找到的每一個質數,我們考慮中間每一個位數被挖空以後,剩下的數字,把他加進去。
  for (int p = 2; p <= 99999; p++)
    if (sieved[p] == 0)
      for (int ten = 1, k = 0; ten <= 10000; ten *= 10, k++)
        cnt[k][ p/(ten*10)*ten + p%ten ]++;
}

int ans, n;
int a[6][6];
int get_count(int x) {
  int prefix = 0;
  for (int i = 0; i < n; i++)
    if (i != x)
      prefix = prefix*10 + a[x][i];
  return cnt[n-1-x][prefix];
}
void dfs(int x, int y, int total) {
  if (x == n) {
    ans += total;
    return;
  }
  if (y == n) {
    dfs(x+1, 0, total * get_count(x));
    return;
  }

  if (x >= y) {
    a[x][y] = a[y][x];
    dfs(x, y+1, total);
  } else {
    for (int i = 0; i <= 9; i++) {
      a[x][y] = i;
      dfs(x, y+1, total);
    }
  }
}

void solve() {
  string s;
  cin >> s;
  n = s.size();
  for (int i = 0; i < n; i++) a[0][i] = (s[i]-'0');
  ans = 0;
  dfs(1, 0, 1);
  cout << ans << endl;
}

int main() {
  int T;
  pre_compute();
  cin >> T;
  while (T--) solve();
  return 0;
}

結論

先枚舉一部分的數值,並試圖留下一些完全不互相影響的待枚舉部分。如此一來,每一個獨立的部分可以各自用預處理的技巧快速找出答案。


例題 2:[CF217B] Blackboard Fibonacci

費氏數列的定義為 $f_0=0, f_1=1, f_n=f_{n-2}+f_{n-1}$。Bajtek 發明了一種在黑板上計算費氏數列的方法:首先在黑板寫下數字 $0$,然後在它下面緊接著寫下數字 $1$。接著,每一次他進行下列其中之一的操作: * 操作 `T`:把上面的數字擦去,並換成兩個數字的總和。 * 操作 `B`:把下面的數字擦去,並換成兩個數字的總和。 如果一切順利,進行了 $n$ 次操作而且從 `T` 開始,將兩種操作交錯進行,那麼最後寫到黑板的數字就會是 $f_{n+1}$。 問題是,Bajtek 在進行操作的時候,常常不小心重複了同一種操作許多次。例如,如果 $n=6$,原本應該要進行的操作順序是 `TBTBTB`,但如果 Bajtek 進行的操作是 `TTTBBT`,那麼會得到 $10$ 這個數字。我們定義「出錯的次數」為序列中該次操作與前一次操作相同的次數。即 `TT` 或 `BB` 出現的總次數。 現在,已知 Bajtek 經過了恰好 $n$ 次操作後計算出了 $r$ 這個數字。請找出「出錯的次數」最少的操作序列,或者輸出無解。$(1\le n, r\le 10^6)$


在第一個例題中,我們試圖降低未枚舉的格子之間的依賴關係。而在第二個例題中,我們試圖找出下一個要枚舉的操作與目前已枚舉部分的關聯,是為增加依賴關係。兩種方法的目的都是為了保證枚舉所花的時間大致與枚舉出的結果總數成正比,減少浪費的時間。


練習題

本文由卡恩(tmt514)撰寫。 本站使用 GasbyJS 搭配 Bulma 製作,其原始碼為 MIT 授權。 網站內容除了題源以外,若無特別說明皆為創用 CC-BY-NC-SA 4.0 授權。 題源部份若有版權爭議還請與我聯繫,感恩。