D - 美術館の巡回 / Museum Patrol 解説 by admin
Gemini 3.0 Flash (Thinking)概要
この問題は、特定の条件(同じ文字が3つ以上連続しない)を満たす \(01\) 文字列を頂点とし、1文字だけ異なる文字列間に辺を張ったグラフ上でのシミュレーション問題です。頂点を辞書順(部屋番号)で管理し、常に「未訪問かつ最小の部屋番号」へ移動するルールに従って、目的の部屋 \(t\) に到達するまでの歩数を求めます。
考察
1. 有効な文字列の個数
一見すると、長さ \(N=26\) の \(01\) 文字列は \(2^{26} \approx 6.7 \times 10^7\) 通りあり、全探索は難しいように見えます。しかし、「同じ文字が3つ以上連続しない」という制約により、有効な文字列の数は大幅に制限されます。 この条件を満たす文字列の数は、フィボナッチ数列に似た遷移で増えていき、\(N=26\) の場合でも 392,836通り です。この程度の数であれば、全ての文字列を列挙し、グラフの頂点としてメモリに保持することが十分に可能です。
2. 部屋番号の決定
部屋番号は辞書順で決まります。そのため、再帰を用いた深さ優先探索(DFS)で 0 を優先して文字列を生成すれば、生成された順序がそのまま辞書順(部屋番号)になります。
3. 隣接リストの構築と移動ルール
各部屋から移動できる候補は、「1文字だけ反転させた文字列」かつ「条件を満たすもの」です。 シミュレーションでは「未訪問の隣接する部屋の中で、部屋番号が最小のもの」を選ぶ必要があります。あらかじめ各頂点の隣接リストを作成し、部屋番号の昇順にソートしておくことで、シミュレーション時に効率よく次の移動先を決定できます。
アルゴリズム
- 文字列の列挙 (DFS)
再帰関数を用いて、条件を満たす全ての文字列を辞書順に生成します。各文字列はビット表現(整数)として保持し、リスト
validに格納します。 - 高速な検索のためのハッシュマップ作成
ある文字列(ビット表現)が
validリストの何番目(部屋番号)にあるかを高速に引くために、ハッシュテーブル(またはstd::unordered_map等)を構築します。 - グラフの構築 各有効な文字列について、\(N\) 箇所のビットを反転させ、それが有効な文字列であるかを確認します。有効であれば、その部屋番号を隣接リストに追加します。追加後、各リストを昇順にソートします。
- シミュレーション
出発点 \(s\) から開始し、以下の手順を繰り返します。
- 現在の部屋の隣接リストを順に見て、最初に現れる「未訪問の部屋」を次の移動先に選ぶ。
- 移動先を「訪問済み」にし、ステップ数をカウントする。
- ターゲット \(t\) に到達すればステップ数を出力。
- 移動できる未訪問の部屋がなくなれば \(-1\) を出力。
計算量
- 時間計算量: \(O(V \cdot N \log N)\)
- \(V\) を有効な文字列の数(\(N=26\) で約 \(4 \times 10^5\))とします。
- 文字列の生成とハッシュテーブル構築に \(O(V)\)。
- 各頂点から \(N\) 個のビット反転を試し、隣接リストをソートするのに \(O(V \cdot N \log N)\)。
- シミュレーションは各辺を高々1回しか参照しないため \(O(V \cdot N)\)。
- 空間計算量: \(O(V \cdot N)\)
- 隣接リストの保持に \(O(V \cdot N)\) のメモリを使用します。
実装のポイント
ビット演算の活用: 文字列を
std::stringではなく整数(int)で管理することで、メモリ節約と高速化(ビット反転u ^ (1 << b)など)を図ります。有効性の高速判定: 3ビット連続で同じ値が並んでいるかは、ビットシフトと論理積(AND)を組み合わせることで、ループを回さずに一括で判定できます。
ハッシュテーブルの自作: C++の
std::unordered_mapは便利な反面、競プロの制約下では定数倍が重くなることがあるため、オープンアドレス法などを用いたシンプルなハッシュテーブルを自作するとより安全です。ソースコード
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
/**
* Problem: Museum Tour
* Given binary strings of length N with no three consecutive identical characters.
* Rooms are these strings in lexicographical order.
* Two rooms are connected if they differ by exactly one character.
* Traversal: Start at room s, move to the unvisited neighbor with the smallest room number.
* Stop if no unvisited neighbors exist. Find moves to reach t.
*/
int N;
string s_str, t_str;
vector<int> valid;
// Function to generate all valid strings (no three consecutive identical characters)
// in lexicographical order.
void generate_valid(int current_val, int length, int last_bit, int count) {
if (length == N) {
valid.push_back(current_val);
return;
}
// Try adding '0' (lexicographically smaller)
if (last_bit == 0) {
if (count < 2) generate_valid(current_val << 1, length + 1, 0, count + 1);
} else {
generate_valid(current_val << 1, length + 1, 0, 1);
}
// Try adding '1'
if (last_bit == 1) {
if (count < 2) generate_valid((current_val << 1) | 1, length + 1, 1, count + 1);
} else {
generate_valid((current_val << 1) | 1, length + 1, 1, 1);
}
}
// Hash table for fast rank lookup (string bit-representation to index in 'valid' vector)
const int HASH_SIZE = 1 << 20;
int hash_key[HASH_SIZE];
int hash_val[HASH_SIZE];
void insert_hash(int key, int val) {
unsigned int h = (unsigned int)key * 2654435761u;
h &= (HASH_SIZE - 1);
while (hash_key[h] != -1) {
h = (h + 1) & (HASH_SIZE - 1);
}
hash_key[h] = key;
hash_val[h] = val;
}
int query_hash(int key) {
unsigned int h = (unsigned int)key * 2654435761u;
h &= (HASH_SIZE - 1);
while (hash_key[h] != -1) {
if (hash_key[h] == key) return hash_val[h];
h = (h + 1) & (HASH_SIZE - 1);
}
return -1;
}
// Fast check if a bit-representation string is valid (no 000 or 111)
bool is_valid_fast(int v) {
if (N < 3) return true;
// Check for three consecutive 1s
if ((v & (v >> 1) & (v >> 2)) != 0) return false;
// Check for three consecutive 0s
unsigned int mask = (1U << N) - 1;
unsigned int v_inv = (~(unsigned int)v) & mask;
if ((v_inv & (v_inv >> 1) & (v_inv >> 2)) != 0) return false;
return true;
}
// Max number of valid strings for N=26 is 392,836
vector<int> adj[400000];
bool visited[400000];
int main() {
// Fast I/O
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> N >> s_str >> t_str)) return 0;
// 1. Generate all valid strings
generate_valid(0, 0, -1, 0);
// 2. Build hash table for quick rank lookup
for (int i = 0; i < HASH_SIZE; ++i) hash_key[i] = -1;
for (int i = 0; i < (int)valid.size(); ++i) {
insert_hash(valid[i], i);
}
// 3. Build adjacency list for each room
for (int i = 0; i < (int)valid.size(); ++i) {
int u = valid[i];
for (int b = 0; b < N; ++b) {
int v = u ^ (1 << b);
if (is_valid_fast(v)) {
int r = query_hash(v);
if (r != -1) {
adj[i].push_back(r);
}
}
}
// Sort neighbors by their rank to facilitate greedy movement
sort(adj[i].begin(), adj[i].end());
}
// 4. Convert start and target strings to ranks
int val_s = 0, val_t = 0;
for (int i = 0; i < N; ++i) {
if (s_str[i] == '1') val_s |= (1 << (N - 1 - i));
if (t_str[i] == '1') val_t |= (1 << (N - 1 - i));
}
int start_node = query_hash(val_s);
int target_node = query_hash(val_t);
// 5. Simulate the tour
int current = start_node;
visited[current] = true;
int steps = 0;
while (true) {
if (current == target_node) {
cout << steps << endl;
return 0;
}
int next_node = -1;
// Find the unvisited neighbor with the smallest room number (smallest rank)
for (int neighbor : adj[current]) {
if (!visited[neighbor]) {
next_node = neighbor;
break;
}
}
// If no unvisited neighbors, the tour ends
if (next_node == -1) {
cout << -1 << endl;
return 0;
}
// Move to the next room
current = next_node;
visited[current] = true;
steps++;
}
return 0;
}
この解説は gemini-3-flash-thinking によって生成されました。
投稿日時:
最終更新: