Count Ways to Build Rooms in an Ant Colony (LeetCode 1916)
Đề bài
1916. Count Ways to Build Rooms in an Ant Colony – Đếm số cách xây các phòng trong tổ kiến (LeetCode, độ khó Hard)
Bạn là một chú kiến được giao nhiệm vụ xây thêm $n$ phòng mới, đánh số từ $0$ đến $n - 1$, cho tổ của mình. Bạn được cho bản kế hoạch mở rộng dưới dạng một mảng số nguyên prevRoom độ dài $n$ (chỉ số tính từ 0), trong đó prevRoom[i] cho biết bạn phải xây phòng prevRoom[i] trước khi xây phòng $i$, và hai phòng này phải được nối trực tiếp với nhau. Phòng $0$ đã được xây sẵn, nên prevRoom[0] = -1. Bản kế hoạch đảm bảo rằng khi xây xong tất cả các phòng thì mọi phòng đều đi tới được từ phòng $0$.
Mỗi lúc bạn chỉ xây được một phòng, và bạn chỉ có thể đi lại tự do giữa các phòng đã xây nếu chúng được nối với nhau. Bạn có thể chọn xây bất kỳ phòng nào miễn là phòng trước nó đã được xây.
Hãy trả về số thứ tự khác nhau để xây tất cả các phòng. Vì đáp án có thể rất lớn, hãy trả về phần dư khi chia cho $10^9 + 7$.
Ràng buộc:
- $n =$
prevRoom.length - $2 \le n \le 10^5$
prevRoom[0] == -1- $0 \le$
prevRoom[i]$\lt n$ với mọi $1 \le i \lt n$ - Khi xây xong tất cả, mọi phòng đều đi tới được từ phòng $0$.
Định dạng vào/ra để chạy thử trên máy: trên LeetCode các bạn chỉ cần viết hàm waysToBuildRooms trong class Solution. Để chạy như một chương trình bình thường (và dùng khung chạy tay bên dưới), mình đặt thêm định dạng:
- Dữ liệu vào: dòng đầu là số nguyên $n$, dòng thứ hai gồm $n$ số nguyên
prevRoom[0], prevRoom[1], ..., prevRoom[n-1](số đầu tiên luôn là $-1$). - Kết quả: một số nguyên là đáp án của bài.
Input: prevRoom = [-1,0,1]
Output: 1
---stdin---
3
-1 0 1Input: prevRoom = [-1,0,0,1,2]
Output: 6
---stdin---
5
-1 0 0 1 20 → 1 → 3 → 2 → 4
0 → 2 → 4 → 1 → 3
0 → 1 → 2 → 3 → 4
0 → 1 → 2 → 4 → 3
0 → 2 → 1 → 3 → 4
0 → 2 → 1 → 4 → 3
1916. Count Ways to Build Rooms in an Ant Colony (LeetCode, Hard)
You are an ant tasked with adding n new rooms numbered 0 to n-1 to your colony. You are given the expansion plan as a 0-indexed integer array of length n, prevRoom, where prevRoom[i] indicates that you must build room prevRoom[i] before building room i, and these two rooms must be connected directly. Room 0 is already built, so prevRoom[0] = -1. The expansion plan is given such that once all the rooms are built, every room will be reachable from room 0.
You can only build one room at a time, and you can travel freely between rooms you have already built only if they are connected. You can choose to build any room as long as its previous room is already built.
Return the number of different orders you can build all the rooms in. Since the answer may be large, return it modulo 10^9 + 7.
Example 1:
Input: prevRoom = [-1,0,1]
Output: 1
Explanation: There is only one way to build the additional rooms: 0 → 1 → 2
Example 2:
Input: prevRoom = [-1,0,0,1,2]
Output: 6
Explanation:
The 6 ways are:
0 → 1 → 3 → 2 → 4
0 → 2 → 4 → 1 → 3
0 → 1 → 2 → 3 → 4
0 → 1 → 2 → 4 → 3
0 → 2 → 1 → 3 → 4
0 → 2 → 1 → 4 → 3
Constraints:
n == prevRoom.length2 <= n <= 10^5prevRoom[0] == -10 <= prevRoom[i] < nfor all1 <= i < n- Every room is reachable from room
0once all the rooms are built.
Lời mở đầu
Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJLưu ý nhỏ: khi nộp trên CLUE OJ các bạn dán toàn bộ code, gồm cả hàm main, vì đề trên CLUE OJ đọc dữ liệu vào và in kết quả đúng theo định dạng ở phần đề bài, giống như khi các bạn chạy thử trên máy hoặc ở trang Chạy code online của blog.
Hiểu đề cho đúng: vì mỗi phòng $i \ne 0$ có đúng một "phòng trước" prevRoom[i] và mọi phòng đều nối về phòng 0, nên các phòng tạo thành một cây có gốc là phòng 0, cha của $i$ là prevRoom[i]. Một thứ tự xây hợp lệ là một cách xếp các đỉnh sao cho cha luôn đứng trước con. Lưu ý prevRoom[i] có thể lớn hơn $i$ (ví dụ [-1, 2, 0]: phòng 2 phải xây trước phòng 1).
Với ví dụ 2, cây như sau: phòng 0 có hai con là 1 và 2; phòng 3 là con của 1, phòng 4 là con của 2. Nhánh trái phải xây theo thứ tự $1 \to 3$, nhánh phải theo thứ tự $2 \to 4$, nhưng ta được phép xen kẽ hai nhánh với nhau – chính 6 cách trong đề là 6 cách xen kẽ đó.
prevRoom = [-1,0,0,1,2]).Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Quay lui thử mọi thứ tự xây – O(n!)
Làm theo đúng đề: đánh dấu built[0] = true. Ở mỗi bước, thử xây mọi phòng $i$ chưa xây mà phòng trước nó (prevRoom[i]) đã xây rồi, đệ quy tiếp, quay lui thì bỏ đánh dấu. Khi đã xây đủ $n$ phòng thì đếm thêm 1 cách.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
int n, cnt;
vector<int> par;
bool built[20];
void backtrack(int k) {
if (k == n) {
cnt++;
return;
}
for (int i = 1; i < n; i++) {
if (!built[i] && built[par[i]]) {
built[i] = true;
backtrack(k + 1);
built[i] = false;
}
}
}
int main() {
cin >> n;
par.resize(n);
for (int i = 0; i < n; i++) cin >> par[i];
built[0] = true;
backtrack(1);
cout << cnt % MOD;
return 0;
}
import sys
MOD = 1000000007
def main():
data = sys.stdin.read().split()
n = int(data[0])
par = list(map(int, data[1:1 + n]))
built = [False] * n
built[0] = True
cnt = 0
def backtrack(k):
nonlocal cnt
if k == n:
cnt += 1
return
for i in range(1, n):
if not built[i] and built[par[i]]:
built[i] = True
backtrack(k + 1)
built[i] = False
backtrack(1)
print(cnt % MOD)
if __name__ == "__main__":
main()
Cách 2Quy hoạch động trên cây + tổ hợp – O(n·log MOD)
Phương pháp giải: Quy hoạch động trên cây + Tổ hợp.
Gọi $f[u]$ là số cách xây (số lộ trình hợp lệ) các phòng trong cây con gốc $u$, và $sz[u]$ là số đỉnh của cây con đó. Xét các cây con gốc $v$ (con của $u$): số cách xây riêng trong cây con gốc $v$ là $f[v]$.
Tuy nhiên, đề nói rằng chúng ta không nhất thiết phải xây xong cây con này rồi mới sang cây con khác: đang xây ở cây con này, ta có thể nhảy sang cây con khác. Như vậy, giả sử với cây con gốc $v_1$ ta đã có một lộ trình, với cây con gốc $v_2$ ta có một lộ trình khác, rồi $v_3, v_4, \ldots$ – ta cần đếm số cách trộn tất cả các lộ trình này lại với nhau sao cho vẫn giữ nguyên thứ tự xuất hiện của các đỉnh trong từng lộ trình.
Ví dụ: lộ trình $X$ có 3 đỉnh X1 → X2 → X3, lộ trình $Y$ có 5 đỉnh Y1 → Y2 → Y3 → Y4 → Y5. Số cách trộn là $C(8, 3) = 56$. Vài lộ trình thỏa mãn:
- Y1 → X1 → X2 → Y2 → X3 → Y3 → Y4 → Y5
- X1 → X2 → X3 → Y1 → Y2 → Y3 → Y4 → Y5
- Y1 → Y2 → Y3 → X1 → X2 → X3 → Y4 → Y5
Chứng minh công thức: trộn 2 lộ trình lại thì có tổng cộng $sz(X) + sz(Y)$ đỉnh, ta xem như có $sz(X) + sz(Y)$ dấu ? xếp hàng: ? → ? → ? → … → ?. Một cách trộn thỏa mãn chính là một cách chọn ra $sz(Y)$ dấu ? để đặt lần lượt các đỉnh của $Y$ vào (theo đúng thứ tự của $Y$), các dấu còn lại đặt lần lượt các đỉnh của $X$. Số cách chọn là $C(sz(X) + sz(Y), sz(Y))$. ∎
Công thức quy hoạch động: duyệt lần lượt từng con $v$ của $u$, cộng dồn $sz[u] \mathrel{+}= sz[v]$ (lúc này $sz[u]$ là tổng số đỉnh các cây con đã trộn, kể cả $v$), rồi:
Trộn xong hết các con thì tăng $sz[u]$ thêm 1 cho chính đỉnh $u$ – đỉnh $u$ luôn phải xây đầu tiên trong cây con của nó nên không có thêm lựa chọn nào. Khởi tạo $f[u] = 1$, đáp án là $f[0]$.
Kiểm tra ví dụ 2: lá 3 và 4 có $f = 1, sz = 1$. Đỉnh 1: $f = 1 \cdot 1 \cdot C(1, 1) = 1$, $sz = 2$; tương tự đỉnh 2: $f = 1$, $sz = 2$. Đỉnh 0: trộn con 1 được $f = 1 \cdot C(2, 2) = 1$, trộn con 2 được $f = 1 \cdot 1 \cdot C(4, 2) = 6$. Đáp án 6 ✔.
POW. Khi đó $C(n, k) = fact[n] \cdot invFact[n - k] \cdot invFact[k] \bmod M$.Các bạn nhập mảng prevRoom rồi bấm Tiếp để xem từng bước trộn (đỉnh vàng là đỉnh đang xử lý, đỉnh xanh là đã tính xong; dưới mỗi đỉnh ghi f và sz):
Code dưới đây của mình đã AC với tư tưởng đã trình bày ở trên, anh em có thể tham khảo nhé:
#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
int sz[100005];
vector<int> child[100005];
long long dp[100005];
long long fact[100005], invFact[100005];
long long POW(long long x, long long n) {
if (n == 0) {
return 1LL;
}
long long tmp = POW(x, n / 2);
(tmp *= tmp) %= MOD;
if (n % 2 == 1) {
(tmp *= x) %= MOD;
}
return tmp;
}
long long C(int n, int k) {
long long ret = fact[n];
(ret *= invFact[n - k]) %= MOD;
(ret *= invFact[k]) %= MOD;
return ret;
}
void init(int n) {
fact[0] = invFact[0] = 1LL;
for (int i = 1; i <= n; i++) {
fact[i] = (fact[i - 1] * (long long) i) % MOD;
invFact[i] = POW(fact[i], MOD - 2);
}
}
void dfs(int u) {
dp[u] = 1;
for (int i = 0; i < (int) child[u].size(); i++) {
int v = child[u][i];
dfs(v);
sz[u] += sz[v];
(dp[u] *= dp[v]) %= MOD;
(dp[u] *= C(sz[u], sz[v])) %= MOD;
}
sz[u]++;
}
class Solution {
public:
int waysToBuildRooms(vector<int>& prevRoom) {
int n = (int) prevRoom.size();
init(n);
memset(dp, 0, sizeof(dp));
memset(sz, 0, sizeof(sz));
for (int i = 0; i < n; i++) {
child[i].clear();
}
for (int i = 1; i < n; i++) {
child[prevRoom[i]].push_back(i);
}
dfs(0);
return (int) dp[0];
}
};
int main() {
int n;
cin >> n;
vector<int> prevRoom(n);
for (int i = 0; i < n; i++) cin >> prevRoom[i];
Solution sol;
cout << sol.waysToBuildRooms(prevRoom);
return 0;
}
import sys
from typing import List
MOD = 1000000007
class Solution:
def waysToBuildRooms(self, prevRoom: List[int]) -> int:
n = len(prevRoom)
fact = [1] * (n + 1)
for i in range(1, n + 1):
fact[i] = fact[i - 1] * i % MOD
inv_fact = [1] * (n + 1)
inv_fact[n] = pow(fact[n], MOD - 2, MOD)
for i in range(n, 0, -1):
inv_fact[i - 1] = inv_fact[i] * i % MOD
def C(a, b):
return fact[a] * inv_fact[a - b] % MOD * inv_fact[b] % MOD
child = [[] for _ in range(n)]
for i in range(1, n):
child[prevRoom[i]].append(i)
# Thứ tự DFS không đệ quy: cha luôn đứng trước con,
# duyệt ngược lại thì con được tính xong trước cha.
order = [0]
for u in order:
order.extend(child[u])
sz = [0] * n
dp = [1] * n
for u in reversed(order):
s = 0
for v in child[u]:
s += sz[v]
dp[u] = dp[u] * dp[v] % MOD * C(s, sz[v]) % MOD
sz[u] = s + 1
return dp[0]
def main():
data = sys.stdin.read().split()
n = int(data[0])
prevRoom = list(map(int, data[1:1 + n]))
print(Solution().waysToBuildRooms(prevRoom))
if __name__ == "__main__":
main()
Giải thích một số chỗ trong code:
init(n)tính sẵn $fact[i]$ và $invFact[i] = $POW(fact[i], MOD - 2)cho $i = 0..n$. Mỗi lần gọiPOWtốn khoảng 30 bước nên tổng cộng $O(n \log M)$.- Trong
dfs(u):sz[u] += sz[v]được làm trước khi nhânC(sz[u], sz[v]), nên lúc đósz[u]chính là $sz(X) + sz(Y)$ như công thức. Dòngsz[u]++đặt sau vòng lặp để cộng chính đỉnh $u$. child[prevRoom[i]].push_back(i): dựng danh sách con. VìprevRoom[i]có thể lớn hơn $i$ nên phải DFS từ gốc chứ không tính theo thứ tự chỉ số được.- Bản Python không dùng đệ quy (cây có thể là một đường thẳng sâu $10^5$ tầng, Python sẽ tràn đệ quy). Mình lấy thứ tự duyệt theo chiều rộng từ gốc (cha luôn đứng trước con) rồi xử lý ngược thứ tự đó, nên con luôn được tính xong trước cha. $invFact$ cũng được tính ngược từ $invFact[n]$ cho nhanh.
Khung chạy tay (để dễ theo dõi, giới hạn $n \le 8$; bước tính POW được gom thành một bước):
Đánh giá độ phức tạp của cách làm này:
- Không gian: các mảng
fact,invFact,dp,sz, danh sách con – $O(n)$. - Thời gian:
initgọiPOW$n$ lần, mỗi lần $O(\log M)$; DFS đi qua mỗi cạnh một lần, mỗi lần tínhCtrong $O(1)$ ⇒ tổng $O(n \log M)$, thực tế coi như $O(n)$.
dfs đệ quy sâu $10^5$ tầng. Code C++ trên LeetCode vẫn chạy ổn (mình thử trên máy với stack 8 MB cũng chạy ổn), nhưng với Python bắt buộc phải khử đệ quy như bản Python ở trên.prevRoom[i] > i), so với code quay lui Cách 1 – khớp hết. Với $n = 10^5$ (đường thẳng, hình sao, ngẫu nhiên), Cách 2 C++ chạy dưới 0.1 giây và Python cho cùng kết quả.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét