Chương 3·Bài 3.4.1

Tree DP

Cho một cây gồm n≤105n \le 10^5 đỉnh. Chọn nhiều đỉnh nhất có thể sao cho không có hai đỉnh được chọn nào nối với nhau bằng một cạnh, trong O(n)O(n).

Ta treo cây xuống từ một đỉnh làm gốc; khi đó mỗi đỉnh có một phần cây nằm dưới nó, gọi là cây con. Với mỗi đỉnh vv, câu trả lời cho cả cây con của vv chỉ cần hai con số: số đỉnh chọn được nhiều nhất khi chọn vv, và khi không chọn vv. Hai con số này tính được từ hai con số của các con của vv, nên chỉ cần đi một lượt từ lá lên gốc. Cách nghĩ này, đặt trạng thái theo cây con rồi tính từ con lên cha, gọi là quy hoạch động trên cây (Tree DP), và là nền cho rất nhiều bài đếm, bài tối ưu trên cây.

ITính từ con lên cha

Cây có nn đỉnh và đúng n−1n - 1 cạnh, giữa hai đỉnh bất kỳ có đúng một đường đi. Sau khi chọn gốc, ta đặt tên các quan hệ: đỉnh ngay phía trên một đỉnh uu là cha của uu, các đỉnh ngay phía dưới là con, và cây con của uu gồm uu cùng mọi đỉnh nằm dưới nó.

Ví dụ đơn giản nhất của quy hoạch động trên cây là kích thước cây con sz[u]\mathrm{sz}[u], tức số đỉnh của cây con của uu. Nó bằng 11 (chính uu) cộng kích thước cây con của từng con của uu. Cái cần cẩn thận duy nhất là thứ tự: khi tính cho uu, mọi con của uu phải đã xong. Hàm đệ quy làm được điều này một cách tự nhiên, nhưng cây có thể là một đường dài 10510^5 đỉnh, và đệ quy sâu chừng ấy tầng có thể làm tràn ngăn xếp. Ta tránh bằng cách duyệt BFS từ gốc, ghi lại thứ tự gặp các đỉnh vào mảng ord và cha của từng đỉnh vào par. Trong thứ tự này, cha luôn đứng trước con của nó, nên nếu đi ngược từ cuối mảng về đầu thì con luôn được xử lý trước cha.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, par[N], ord[N], sz[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll v : g[u])
            if (v != par[u]) par[v] = u, ord[++m] = v;
    }
    for (ll i = 1; i <= n; i++) sz[i] = 1;
    for (ll i = n; i >= 2; i--) sz[par[ord[i]]] += sz[ord[i]];
    for (ll i = 1; i <= n; i++) cout << sz[i] << ' ';
    cout << '\n';
}
5
1 2
1 3
2 4
2 5

Độ phức tạp: O(n)O(n), với n=105n = 10^5 cần khoảng 4⋅1054 \cdot 10^5 phép tính.

Hỏi nhanh

Một bạn viết nhầm vòng cuối của chương trình trên thành for (ll i = 2; i <= n; i++) sz[par[ord[i]]] += sz[ord[i]];, tức duyệt xuôi thay vì ngược. Cây là đường 1−2−31 - 2 - 3 với gốc 11. Chương trình in ra sz[1]\mathrm{sz}[1] bằng bao nhiêu?

Ví dụChọn Đại Diện Sửa Đường

Đề. Có n≤105n \le 10^5 khu vực nối bởi n−1n - 1 con đường hai chiều thành một cây, trụ sở ở khu vực 11. Mỗi đường thuộc loại 11 (bình thường) hoặc loại 22 (cần sửa). Chọn một khu vực vv nghĩa là cử một người từ vv đi theo đường duy nhất về khu vực 11 và sửa mọi đường loại 22 trên đường đi. Hãy chọn ít khu vực nhất để mọi đường loại 22 đều được sửa, rồi in số khu vực và một danh sách.

Lời giải. Treo cây từ khu vực 11 và xét một đường loại 22 nối đỉnh vv với cha của nó. Người sửa đường này phải xuất phát từ cây con của vv. Nếu cây con của vv đã có một khu vực được chọn, người đó đi về gốc chắc chắn qua đường này, ta không cần làm gì thêm. Nếu chưa có, ta buộc phải chọn thêm một khu vực trong cây con của vv, và chọn ngay vv là không kém: người ở vv sửa đúng đường này, còn các đường phía trên mà một người ở sâu hơn sửa được thì người ở vv cũng sửa được. Mỗi lần chọn như vậy là bắt buộc nên số lượng là ít nhất.

Vậy trạng thái của vv chỉ là một cờ: cây con của vv đã có khu vực được chọn hay chưa. Ta duyệt ngược thứ tự BFS; xử lý xong vv thì báo lên cha của vv nếu cây con của vv đã có người. Đề cho in danh sách theo thứ tự bất kỳ, nhưng ta sắp tăng dần cho gọn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, par[N], typ[N], ord[N], has[N];
vector<pair<ll, ll> > g[N];
vector<ll> ans;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v, t;
        cin >> u >> v >> t;
        g[u].push_back(make_pair(v, t));
        g[v].push_back(make_pair(u, t));
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll j = 0; j < (ll)g[u].size(); j++) {
            ll v = g[u][j].first;
            if (v == par[u]) continue;
            par[v] = u;
            typ[v] = g[u][j].second;
            ord[++m] = v;
        }
    }
    for (ll i = n; i >= 2; i--) {
        ll u = ord[i];
        if (typ[u] == 2 && !has[u]) {
            has[u] = 1;
            ans.push_back(u);
        }
        if (has[u]) has[par[u]] = 1;
    }
    sort(ans.begin(), ans.end());
    cout << ans.size() << '\n';
    for (ll i = 0; i < (ll)ans.size(); i++) cout << ans[i] << ' ';
    cout << '\n';
}
7
1 2 2
2 3 1
2 4 2
1 5 1
5 6 2
6 7 1

Độ phức tạp: O(n)O(n) cho BFS và một lượt ngược, với n=105n = 10^5 cần khoảng 5⋅1055 \cdot 10^5 phép tính, cộng O(nlog⁡n)O(n \log n) cho bước sắp xếp danh sách.

IIChọn hoặc không chọn đỉnh gốc của cây con

Ta quay lại bài toán đầu: chọn nhiều đỉnh nhất sao cho không có hai đỉnh được chọn kề nhau. Cách nghĩ vét cạn là thử mọi tập con, 2n2^n phương án. Ta có thể thấy cây con của uu chỉ nối với phần còn lại của cây qua một cạnh duy nhất, cạnh từ uu lên cha. Vì vậy, muốn biết một cách chọn trong cây con của uu có ghép được với cách chọn bên ngoài hay không, chỉ cần biết uu có được chọn hay không. Với mỗi đỉnh uu ta giữ hai con số:

  • f1[u]f_1[u] là số đỉnh chọn được nhiều nhất trong cây con của uu khi uu được chọn;
  • f0[u]f_0[u] là số đó khi uu không được chọn.

Nếu chọn uu thì không con nào của uu được chọn, nên f1[u]f_1[u] bằng 11 cộng tổng f0f_0 của các con. Nếu không chọn uu thì mỗi con tự do chọn hay không, ta lấy cách tốt hơn cho từng con: f0[u]f_0[u] bằng tổng max⁡(f0[v],f1[v])\max(f_0[v], f_1[v]) qua các con vv. Đáp án là max⁡(f0[1],f1[1])\max(f_0[1], f_1[1]). Khi mỗi đỉnh có thêm giá trị thì thay số 11 bằng giá trị của đỉnh đó.

Nhãn trên đỉnh là k/c: số đỉnh chọn được nhiều nhất trong cây con khi không chọn, hoặc khi chọn đỉnh đó.

Chương trình dưới đây tính cho cây trong hình. Mỗi đỉnh lấy dữ liệu từ các con của nó, đi theo thứ tự ngược của BFS.

C++
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, par[N], ord[N], f0[N], f1[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll v : g[u])
            if (v != par[u]) par[v] = u, ord[++m] = v;
    }
    for (ll i = n; i >= 1; i--) {
        ll u = ord[i];
        f1[u] = 1;
        for (ll v : g[u]) {
            if (v == par[u]) continue;
            f0[u] += max(f0[v], f1[v]);
            f1[u] += f0[v];
        }
    }
    cout << max(f0[1], f1[1]) << '\n';
}
7
1 2
1 3
2 4
2 5
3 6
5 7

Độ phức tạp: O(n)O(n), vì mỗi cạnh được xét hai lần, với n=105n = 10^5 cần khoảng 10610^6 phép tính.

Sử dụng kiến thức này cũng có thể giải bài phủ đỉnh nhỏ nhất (chọn ít đỉnh nhất để mỗi cạnh có ít nhất một đầu được chọn): các đỉnh không được chọn khi đó không có hai đỉnh nào kề nhau, nên đáp án bằng nn trừ đi kích thước tập độc lập lớn nhất.

Ví dụGhép Cạnh Trên Cây

Đề. Cho một cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh. Chọn nhiều cạnh nhất có thể sao cho không có hai cạnh được chọn cùng đi qua một đỉnh (một cặp ghép của cây). In số cạnh đó.

Lời giải. Cũng hai trạng thái cho mỗi đỉnh uu, nhưng ý nghĩa khác: d0[u]d_0[u] là số cạnh nhiều nhất trong cây con của uu khi uu không ghép với con nào, và d1[u]d_1[u] là số đó khi uu ghép với đúng một con. Đặt bb là tổng của max⁡(d0[v],d1[v])\max(d_0[v], d_1[v]) qua các con vv của uu. Nếu uu không ghép với con nào thì mỗi con tự do chọn, nên d0[u]=bd_0[u] = b. Nếu uu ghép với con vv thì cạnh (u,v)(u, v) được thêm vào, vv bắt buộc ở trạng thái chưa ghép với con của nó, còn các con khác tự do:

d1[u]=max⁡v(b−max⁡(d0[v],d1[v])+d0[v]+1).d_1[u] = \max_{v} \bigl( b - \max(d_0[v], d_1[v]) + d_0[v] + 1 \bigr).

Đỉnh lá không có con nên không thể ghép xuống; ta gán d1=−1018d_1 = -10^{18} cho nó để trạng thái này không bao giờ được chọn.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5;
ll n, par[N], ord[N], d0[N], d1[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll v : g[u])
            if (v != par[u]) par[v] = u, ord[++m] = v;
    }
    for (ll i = n; i >= 1; i--) {
        ll u = ord[i], b = 0;
        for (ll v : g[u])
            if (v != par[u]) b += max(d0[v], d1[v]);
        d0[u] = b;
        d1[u] = -1e18;
        for (ll v : g[u])
            if (v != par[u]) d1[u] = max(d1[u], b - max(d0[v], d1[v]) + d0[v] + 1);
    }
    cout << max(d0[1], d1[1]) << '\n';
}
5
1 2
2 3
2 4
4 5

Độ phức tạp: O(n)O(n), với n=2⋅105n = 2 \cdot 10^5 cần khoảng 10610^6 phép tính.

Hỏi nhanh

Đỉnh uu có hai con. Con thứ nhất có (d0,d1)=(2,3)(d_0, d_1) = (2, 3), con thứ hai có (d0,d1)=(1,1)(d_0, d_1) = (1, 1). Tính (d0[u],d1[u])(d_0[u], d_1[u]) cho bài ghép cặp lớn nhất.

IIIĐường kính bằng quy hoạch động

Đường kính của cây là đường đi dài nhất giữa hai đỉnh. Cho một cây mà cạnh (u,v)(u, v) có độ dài w(u,v)≥0w(u, v) \ge 0; ta muốn tìm độ dài của đường kính. Hai lần BFS ở bài 3.1 làm được khi mọi cạnh có độ dài 11; ở đây ta tìm bằng quy hoạch động để dùng được cho độ dài tùy ý không âm.

Mỗi đường đi trên cây treo gốc có một đỉnh cao nhất, là đỉnh gần gốc nhất của nó, và đường đi gồm hai nhánh đi xuống từ đỉnh đó, qua hai con khác nhau (hoặc chỉ một nhánh). Gọi h[u]h[u] là độ dài nhánh đi xuống dài nhất từ uu, h[u]=0h[u] = 0 nếu uu là lá. Với mỗi con vv của uu, nhánh đi qua vv có độ dài h[v]+w(u,v)h[v] + w(u, v). Hai nhánh tốt nhất qua hai con khác nhau cho đường dài nhất có đỉnh cao nhất là uu, và đường kính là giá trị lớn nhất của đại lượng này qua mọi uu.

h là số cạnh của nhánh đi xuống dài nhất; đường kính (cam) ghép hai nhánh dài 3 và 2 tại đỉnh 1.6452731h = 0h = 1h = 0h = 2h = 0h = 1h = 3
h là số cạnh của nhánh đi xuống dài nhất; đường kính (cam) ghép hai nhánh dài 3 và 2 tại đỉnh 1.

Để lấy hai nhánh tốt nhất, ta giữ hai biến m1≥m2m_1 \ge m_2 khi lướt qua các con: nhánh tốt nhất và nhánh tốt nhì. Khi uu có một con thì m2=0m_2 = 0 và đường kính qua uu chỉ là một nhánh.

Hỏi nhanh

Đỉnh uu có ba con, các nhánh đi qua chúng dài 55, 33 và 44. Đường dài nhất mà đỉnh cao nhất là uu dài bao nhiêu?

Ví dụĐồng Nhất Màu Trên Cây

Đề. Cho cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh, mỗi đỉnh có màu 00 hoặc 11. Một thao tác chọn một đỉnh vv và đảo màu toàn bộ thành phần đơn sắc đang chứa vv, tức tập các đỉnh nối với vv bằng đường đi chỉ gồm đỉnh cùng màu với vv. Tìm số thao tác ít nhất để mọi đỉnh cùng màu.

Lời giải. Gán cho mỗi cạnh độ dài 11 nếu hai đầu khác màu và độ dài 00 nếu cùng màu. Khi đó độ dài của một đường đi là số lần màu đổi dọc đường. Gọi DD là đường kính theo độ dài này. Ta sẽ chứng minh đáp án là ⌈D/2⌉\lceil D/2 \rceil, tức (D+1)/2(D + 1)/2 với phép chia nguyên.

Chứng minh

Gộp mỗi thành phần đơn sắc thành một đỉnh, ta được một cây mà hai đỉnh kề luôn khác màu; đường kính của cây này, tính theo số cạnh, đúng bằng DD. Một thao tác đảo một đỉnh của cây gộp làm nó nhập với tất cả các đỉnh kề, nên đường kính giảm không quá 22. Cần đưa đường kính về 00, nên cần ít nhất ⌈D/2⌉\lceil D/2 \rceil thao tác. Ngược lại, cứ đảo đỉnh ở tâm của cây gộp thì bán kính giảm đúng 11 sau mỗi thao tác, và bán kính của cây gộp là ⌈D/2⌉\lceil D/2 \rceil, nên đủ ⌈D/2⌉\lceil D/2 \rceil thao tác.

Còn lại là tính DD bằng quy hoạch động ở trên. Với D=3D = 3 trong ví dụ mẫu (màu 0,0,1,1,0,10, 0, 1, 1, 0, 1 trên đường 1−2−3−4−5−61 - 2 - 3 - 4 - 5 - 6, màu đổi ba lần), đáp án là 22.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5;
ll n, c[N], par[N], ord[N], h[N];
vector<ll> g[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> c[i];
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll v : g[u])
            if (v != par[u]) par[v] = u, ord[++m] = v;
    }
    ll d = 0;
    for (ll i = n; i >= 1; i--) {
        ll u = ord[i], m1 = 0, m2 = 0;
        for (ll v : g[u]) {
            if (v == par[u]) continue;
            ll x = h[v] + (c[u] != c[v]);
            if (x > m1) m2 = m1, m1 = x;
            else m2 = max(m2, x);
        }
        h[u] = m1;
        d = max(d, m1 + m2);
    }
    cout << (d + 1) / 2 << '\n';
}
6
0 0 1 1 0 1
1 2
2 3
3 4
4 5
5 6

Độ phức tạp: O(n)O(n), với n=2⋅105n = 2 \cdot 10^5 cần khoảng 10610^6 phép tính.

IVHai cây con có giống nhau không

Hai cây con có gốc được gọi là đẳng cấu nếu ta có thể đổi tên các đỉnh của cây này thành đỉnh của cây kia sao cho gốc ứng với gốc và quan hệ cha con được giữ nguyên; nói cách khác chúng giống hệt nhau khi bỏ qua số hiệu đỉnh và thứ tự các con. Muốn so sánh nhiều cây con, ta gán cho mỗi cây con một mã: hai cây con có cùng mã khi và chỉ khi chúng đẳng cấu.

Cây con của uu hoàn toàn được xác định bởi các cây con của các con của uu, và thứ tự các con không quan trọng. Vậy mã của uu được xác định bởi đa tập mã của các con (tập có thể lặp phần tử). Ta lấy danh sách mã của các con, sắp xếp tăng dần để thứ tự không ảnh hưởng, rồi tra trong một bảng map từ danh sách sang mã: danh sách đã có thì dùng lại mã cũ, chưa có thì cấp mã mới. Lá có danh sách rỗng nên mọi lá cùng một mã.

Câu hỏi nhanh dành cho tài khoản Pro

Mở khóa bằng tài khoản Pro

Ví dụPhân Lớp Các Cây Con Có Gốc

Đề. Cho cây có gốc 00 gồm n≤5⋅105n \le 5 \cdot 10^5 đỉnh, đỉnh v>0v > 0 có cha pv<vp_v < v. Hãy chia các cây con có gốc tại từng đỉnh thành các lớp đẳng cấu: in số lớp kk, rồi nn số c0,…,cn−1c_0, \dots, c_{n-1} với 0≤cv<k0 \le c_v < k sao cho cu=cvc_u = c_v khi và chỉ khi hai cây con tại uu và vv đẳng cấu.

Lời giải. Vì cha có chỉ số nhỏ hơn con, đi vv từ n−1n - 1 xuống 00 thì mọi con của vv đã xong. Mỗi đỉnh xong thì mã của nó được đẩy vào danh sách của cha. Tổng độ dài các danh sách là n−1n - 1, bảng map có khoá là danh sách nên mỗi lần tra tốn thời gian tỉ lệ với độ dài danh sách nhân log⁡n\log n.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 5e5 + 5;
ll n, p[N], c[N];
vector<ll> ch[N];
map<vector<ll>, ll> id;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) cin >> p[i];
    for (ll v = n - 1; v >= 0; v--) {
        sort(ch[v].begin(), ch[v].end());
        if (!id.count(ch[v])) {
            ll k = id.size();
            id[ch[v]] = k;
        }
        c[v] = id[ch[v]];
        if (v > 0) ch[p[v]].push_back(c[v]);
    }
    cout << id.size() << '\n';
    for (ll v = 0; v < n; v++) cout << c[v] << ' ';
    cout << '\n';
}
7
0 0 1 1 2 2

Độ phức tạp: O(nlog⁡n)O(n \log n), với n=5⋅105n = 5 \cdot 10^5 cần khoảng 10710^7 phép tính.

Bài nâng cao

Xem 1 bài nâng cao

Ví dụKhung Phân Nhánh Bậc Bốn

Đề. Cho cây TT gồm n≤2⋅105n \le 2 \cdot 10^5 đỉnh. Chọn một số đỉnh và cạnh của TT để được cây con liên thông HH mà mọi đỉnh của HH có bậc đúng 11 hoặc đúng 44 (tính theo cạnh của HH) và HH có ít nhất một đỉnh bậc 44. Tìm số đỉnh lớn nhất của HH, hoặc in −1-1 nếu không có.

Lời giải. Treo cây từ đỉnh 11. Trong HH, đỉnh cao nhất ww không có cha trong HH, nên bậc của nó chính là số con của nó trong HH. Với mỗi đỉnh uu ta tính g[u]g[u], số đỉnh lớn nhất của một “mảnh” gốc uu trong đó uu đã có cạnh nối lên cha: hoặc uu là lá của HH (mảnh chỉ có uu, kích thước 11), hoặc uu có bậc 44, nghĩa là ba con của uu cùng với mảnh của chúng. Gọi $q[u] = 1 + $ tổng ba giá trị gg lớn nhất của các con của uu (cần uu có ít nhất ba con), ta có g[u]=max⁡(1,q[u])g[u] = \max(1, q[u]). Đỉnh cao nhất ww của HH rơi vào một trong hai trường hợp: nó có bậc 44, cho 1+1 + tổng bốn giá trị gg lớn nhất của các con; hoặc nó là lá của HH với một con cc có bậc 44, cho 1+q[c]1 + q[c]. Đáp án là giá trị lớn nhất qua mọi ww trong các trường hợp có xảy ra; nếu không trường hợp nào xảy ra thì in −1-1. Việc lấy các giá trị lớn nhất ở mỗi đỉnh là sắp xếp danh sách gg của các con giảm dần.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5;
ll n, par[N], ord[N], g[N], q[N];
vector<ll> adj[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    ll m = 0;
    ord[++m] = 1;
    for (ll i = 1; i <= m; i++) {
        ll u = ord[i];
        for (ll v : adj[u])
            if (v != par[u]) par[v] = u, ord[++m] = v;
    }
    ll ans = -1;
    for (ll i = n; i >= 1; i--) {
        ll u = ord[i];
        vector<ll> val;
        for (ll v : adj[u])
            if (v != par[u]) val.push_back(g[v]);
        sort(val.begin(), val.end(), greater<ll>());
        q[u] = -1e18;
        if (val.size() >= 3) q[u] = 1 + val[0] + val[1] + val[2];
        g[u] = max(1LL, q[u]);
        if (val.size() >= 4) ans = max(ans, 1 + val[0] + val[1] + val[2] + val[3]);
        for (ll v : adj[u])
            if (v != par[u] && q[v] > 0) ans = max(ans, 1 + q[v]);
    }
    cout << ans << '\n';
}
10
1 2
1 3
1 4
1 5
5 6
5 7
5 8
5 9
9 10

Độ phức tạp: O(nlog⁡n)O(n \log n) cho các lần sắp xếp danh sách con, với n=2⋅105n = 2 \cdot 10^5 cần khoảng 4⋅1064 \cdot 10^6 phép tính.

Khi đi thi

  • Viết rõ trạng thái là “đáp án cho cây con của uu” cộng thêm những cờ nào về chính uu (được chọn hay không, đã ghép hay chưa), rồi mới viết công thức.
  • Cây có thể là một đường dài 10510^5 đỉnh thì đệ quy có thể tràn ngăn xếp; dùng thứ tự BFS và đi ngược.
  • Khi đáp án ở một đỉnh ghép từ nhiều con (đường kính, ghép cặp), giữ một hai giá trị tốt nhất của các con thay vì thử mọi cặp con.

Tóm tắt

  • Quy hoạch động trên cây tính đáp án của một đỉnh từ đáp án của các con; thứ tự duy nhất cần giữ là con xong trước cha, làm được bằng cách đi ngược thứ tự BFS.
  • Khi mỗi đỉnh có hai lựa chọn, ta giữ hai con số, mỗi lựa chọn một số; tập độc lập và cặp ghép lớn nhất đều theo khuôn này.
  • Đường kính cho một đường có hai nhánh đi xuống từ đỉnh cao nhất; giữ nhánh tốt nhất và nhánh tốt nhì khi lướt qua các con.
  • Phủ đỉnh nhỏ nhất bằng nn trừ kích thước tập độc lập lớn nhất.
  • Hai cây con đẳng cấu khi đa tập mã các con trùng nhau; sắp xếp danh sách mã rồi tra bảng map để gán mã.

Bài tập

Đọc thêm