DP ағашта деген не?
DP ағашта — бұл динамикалық программалау. Мұнда әр вершина үшін answer немесе state есептеледі.
Көбіне біз DFS жүргіземіз және әр вершина поддеревосы үшін dp[v] мәнін санаймыз.
кейін солар арқылы parent жауабын санаймыз
Ағаш деген не?
Ағаш — connected graph және ішінде cycle жоқ.
Егер ағашта n вершина болса, онда ребро саны әрқашан n - 1.
edge саны = n - 1
Ағаштың root-ы
DP жасау үшін ағашты бір root-қа байлап аламыз. Көбіне root ретінде 1 вершинасын аламыз.
dfs(1, 0);
Сонда әр вершина үшін:
- parent — ата-анасы
- children — балалары
- subtree — сол вершинадан төмен тұрған бөлік
Ағаштағы базовый DFS
Ағашта cycle жоқ. Бірақ реброларды екі жаққа сақтасақ, parent-қа қайта кірмеу керек.
void dfs(int v, int p) {
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
}
}
Мұнда:
- v — қазіргі вершина
- p — parent вершина
- to — көрші вершина
Tree DP негізгі схемасы
void dfs(int v, int p) {
// dp[v] үшін бастапқы мән
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
// dp[v] мәнін dp[to] арқылы жаңартамыз
}
}
Задача 1: поддерево size
Әр вершина үшін оның поддерево size-ын табу керек.
State:
dp[v] = v вершинасының поддерево size-ы
Әр вершина өзі size-қа 1 береді:
dp[v] = 1;
Кейін барлық child size-ын қосамыз:
dp[v] += dp[to];
Код: поддерево size
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
int dp[N];
void dfs(int v, int p) {
dp[v] = 1;
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
dp[v] += dp[to];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
for (int i = 1; i <= n; i++) {
cout << dp[i] << ' ';
}
return 0;
}
Поддерево size мысалы
Input
5
1 2
1 3
3 4
3 5
Root 1 болса, ағаш былай:
├── 2
└── 3
├── 4
└── 5
Поддерево size:
dp[2] = 1
dp[3] = 3
dp[4] = 1
dp[5] = 1
Задача 2: көрші емес вершинадан максималды сумма
Ағаш берілген. Әр вершинада w[v] weight бар. Вершиналарды таңдау керек, бірақ көрші екі вершина бірге таңдалмауы керек. Сумма максималды болсын.
Бұл Maximum Independent Set on Tree деп аталады.
онда оның child-тарын таңдай алмаймыз
Independent Set state-тері
Әр вершина үшін екі мән сақтаймыз:
dp[v][0]
dp[v][1]
- dp[v][0] — v таңдалмаған кездегі v поддеревосындағы max answer
- dp[v][1] — v таңдалған кездегі v поддеревосындағы max answer
Переход
Егер v таңдалса, child таңдалмайды:
dp[v][1] += dp[to][0];
Егер v таңдалмаса, әр child таңдалуы да, таңдалмауы да мүмкін:
dp[v][0] += max(dp[to][0], dp[to][1]);
Бастапқы мән:
dp[v][1] = w[v];
dp[v][0] = 0;
Код: Maximum Independent Set
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
long long w[N];
long long dp[N][2];
void dfs(int v, int p) {
dp[v][0] = 0;
dp[v][1] = w[v];
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
dp[v][0] += max(dp[to][0], dp[to][1]);
dp[v][1] += dp[to][0];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> w[i];
}
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
cout << max(dp[1][0], dp[1][1]);
return 0;
}
Мысал
Input
5
10 1 5 7 8
1 2
1 3
3 4
3 5
Таңдауға болады:
сумма = 10 + 7 + 8 = 25
Output
25
Задача 3: Minimum Vertex Cover
Минималды вершина санын таңдау керек. Әр реброның кемінде бір ұшы таңдалған болуы керек.
Бұл Minimum Vertex Cover on Tree.
a немесе b таңдалуы керек
Vertex Cover state-тері
dp[v][0]
dp[v][1]
- dp[v][0] — v таңдалмаған кездегі minimum answer
- dp[v][1] — v таңдалған кездегі minimum answer
Егер v таңдалмаса, барлық child міндетті түрде таңдалуы керек.
dp[v][0] += dp[to][1];
Егер v таңдалса, child таңдалуы да, таңдалмауы да мүмкін.
dp[v][1] += min(dp[to][0], dp[to][1]);
Код: Minimum Vertex Cover
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
int dp[N][2];
void dfs(int v, int p) {
dp[v][0] = 0;
dp[v][1] = 1;
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
dp[v][0] += dp[to][1];
dp[v][1] += min(dp[to][0], dp[to][1]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
cout << min(dp[1][0], dp[1][1]);
return 0;
}
Задача 4: поддерево биіктігі
Әр вершина үшін оның поддерево биіктігін табу керек.
State:
dp[v] = v поддеревосының биіктігі
Егер вершинада child жоқ болса, биіктігі 1.
Әйтпесе:
dp[v] = max(dp[to] + 1)
Код: поддерево биіктігі
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
int dp[N];
void dfs(int v, int p) {
dp[v] = 1;
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
dp[v] = max(dp[v], dp[to] + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
for (int i = 1; i <= n; i++) {
cout << dp[i] << ' ';
}
return 0;
}
Задача 5: tree diameter арқылы DP
Tree diameter — ағаштағы екі вершина арасындағы ең ұзын distance.
Оны DP арқылы табуға болады.
Әр вершина үшін child-тардан келетін екі ең үлкен height сақтаймыз.
бір поддереводан екінші поддеревоға
Диаметр идеясы
best1 және best2 — child-тардан келетін екі максималды depth.
v арқылы өтетін possible diameter:
best1 + best2
Ал parent үшін height:
dp[v] = best1;
Код: ағаш диаметрі
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
int dp[N];
int ans = 0;
void dfs(int v, int p) {
int best1 = 0;
int best2 = 0;
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
int cur = dp[to] + 1;
if (cur > best1) {
best2 = best1;
best1 = cur;
} else if (cur > best2) {
best2 = cur;
}
}
dp[v] = best1;
ans = max(ans, best1 + best2);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 0);
cout << ans;
return 0;
}
Tree DP рекурсивный метод
Tree DP көбіне рекурсия арқылы жазылады.
Біз DFS шақырамыз:
dfs(v, parent)
DFS ішінде dp[v] санаймыз.
Бұл ыңғайлы, себебі ағаш өзі дұрыс order береді:
parent кейін есептеледі
root ең соңында есептеледі
Итеративный методқа қалай ауыстырамыз?
Кейде recursion depth үлкен болып, stack overflow болуы мүмкін. Онда итеративный DFS қолдануға болады.
Идея:
2. Кейін order-ды reverse жасаймыз
3. Сонда child parent-тен бұрын өңделеді
DP үшін итеративный order
vector<int> order;
vector<int> parent(n + 1, 0);
stack<int> st;
st.push(1);
parent[1] = -1;
while (!st.empty()) {
int v = st.top();
st.pop();
order.push_back(v);
for (int to : g[v]) {
if (to == parent[v]) {
continue;
}
parent[to] = v;
st.push(to);
}
}
reverse(order.begin(), order.end());
reverse жасағаннан кейін алдымен children, кейін parents келеді. Бұл DFS postorder сияқты.
Поддерево size үшін итеративный DP
for (int v : order) {
dp[v] = 1;
for (int to : g[v]) {
if (to == parent[v]) {
continue;
}
dp[v] += dp[to];
}
}
Итеративный толық реализация
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
vector<int> g[N];
int parent[N];
int dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
vector<int> order;
stack<int> st;
st.push(1);
parent[1] = -1;
while (!st.empty()) {
int v = st.top();
st.pop();
order.push_back(v);
for (int to : g[v]) {
if (to == parent[v]) {
continue;
}
parent[to] = v;
st.push(to);
}
}
reverse(order.begin(), order.end());
for (int v : order) {
dp[v] = 1;
for (int to : g[v]) {
if (to == parent[v]) {
continue;
}
dp[v] += dp[to];
}
}
for (int i = 1; i <= n; i++) {
cout << dp[i] << ' ';
}
return 0;
}
Tree DP қалай ойлап табамыз?
Көбіне мына сұрақтарға жауап береміз:
2. 0/1 state керек пе?
3. Егер v таңдалса не болады?
4. Егер v таңдалмаса не болады?
5. Child answers қалай біріктіріледі?
6. Answer dp[1]-де ме, әлде max/min керек пе?
Типтік state-тер
| State | Мағынасы |
|---|---|
| dp[v] | v поддеревосы үшін answer |
| dp[v][0] | v таңдалмаған кездегі answer |
| dp[v][1] | v таңдалған кездегі answer |
| subtree[v] | v поддеревосының size-ы |
| height[v] | v поддеревосының height-ы |
Типтік қателер
- DFS-қа parent бермеу
- Parent-қа қайта кіріп, infinite recursion алу
- dfs(to) алдында dp[v] жаңарту
- dp[v] үшін base value ұмыту
- dp[v][0] және dp[v][1] шатастыру
- Answer max/min керек кезде тек dp[1][1] алу
- Үлкен ағашта stack overflow алу
Асимптотика
| Задача | Уақыт | Память |
|---|---|---|
| Поддерево size | O(n) | O(n) |
| Independent Set | O(n) | O(n) |
| Vertex Cover | O(n) | O(n) |
| Diameter | O(n) | O(n) |
Қысқа Tree DP шаблон
void dfs(int v, int p) {
// base dp[v]
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
// update dp[v] using dp[to]
}
}
Қысқа dp[v][0/1] шаблон
void dfs(int v, int p) {
dp[v][0] = base0;
dp[v][1] = base1;
for (int to : g[v]) {
if (to == p) {
continue;
}
dfs(to, v);
dp[v][0] += transition_when_v_not_taken;
dp[v][1] += transition_when_v_taken;
}
}