kz-maxx

Root-ты ауыстыру

Rerooting DP: ағаштың әр вершинасын root қылып answer санау

Root-ты ауыстыру деген не?

Root-ты ауыстыру, немесе rerooting, — Tree DP әдісі. Ол answer бір root үшін ғана емес, әр вершина root болған жағдай үшін керек кезде қолданылады.

алдымен root = 1 үшін answer санаймыз
кейін root-ты v вершинасынан child to вершинасына ауыстырамыз
answer-ды тез пересчет жасаймыз
Негізгі идея: әр вершинадан қайта DFS жасамаймыз. Әдетте 2 DFS арқылы барлық вершина үшін answer аламыз.

Неге керек?

Егер әр вершинадан бөлек DFS жасасақ, өте баяу болады.

Мысалы, n = 100000 болса:

әр вершинадан DFS → O(n²)
rerooting → O(n)

Сондықтан rerooting көбіне answer барлық вершина үшін керек болғанда қолданылады.

Идея қарапайым тілмен

Бізде v вершинасы үшін answer белгілі болсын. Енді оның child-ы to үшін answer тапқымыз келеді.

Root ауысқанда ағаш былай өзгереді:

бұрын: v — parent, to — child
кейін: to — жаңа root, v — child

Біз answer қалай өзгеретінін түсінуіміз керек. Көп задачада оны O(1) формуламен жасауға болады.

Классикалық задача

n вершинадан тұратын ағаш берілген. Әр вершина v үшін v-дан барлық басқа вершинаға дейінгі distance суммасын табу керек.

Яғни:

ans[v] = dist(v, 1) + dist(v, 2) + ... + dist(v, n)

Мұны барлық v үшін санау керек.

Мысал

Ағаш:

1 -- 2
1 -- 3
3 -- 4
3 -- 5

Егер вершина 1-ді алсақ:

dist(1,1) = 0
dist(1,2) = 1
dist(1,3) = 1
dist(1,4) = 2
dist(1,5) = 2
сумма = 6

Демек:

ans[1] = 6

Не сақтаймыз?

Бізге үш массив керек:

sz[v]
dp[v]
ans[v]
  • sz[v] — v вершинасының поддерево size-ы
  • dp[v] — v-дан өзінің поддеревосындағы вершинаға дейінгі distance суммасы
  • ans[v] — v-дан барлық вершинаға дейінгі distance суммасы

Бірінші DFS

Алдымен ағашты root = 1 қылып аламыз. Бірінші DFS ішінде санаймыз:

  • sz[v]
  • dp[v]

Әр вершина үшін бастапқы мән:

sz[v] = 1;

Кейін child-тардан қосамыз:

sz[v] += sz[to];
dp[v] += dp[to] + sz[to];
Неге dp[to] + sz[to]? to поддеревосындағы барлық вершина v-дан қарағанда 1 реброға алысырақ болады.

Бірінші DFS коды

void dfs1(int v, int p) {
    sz[v] = 1;
    dp[v] = 0;

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        dfs1(to, v);

        sz[v] += sz[to];
        dp[v] += dp[to] + sz[to];
    }
}

Осыдан кейін:

ans[1] = dp[1];

Себебі dp[1] — root 1-ден барлық вершинаға дейінгі distance суммасы.

Rerooting-тің негізгі формуласы

Root-ты v-дан оның child-ы to-ға ауыстырамыз.

Сонда:

ans[to] = ans[v] - sz[to] + (n - sz[to]);

Бұл ең маңызды формула.

Неге формула осындай?

Root v-дан to-ға ауысқанда:

  • to поддеревосындағы барлық вершина 1-ге жақындайды
  • ондай вершина саны sz[to]
  • сондықтан сумма sz[to]-ға азаяды
  • қалған барлық вершина 1-ге алыстайды
  • ондай вершина саны n - sz[to]
  • сондықтан сумма n - sz[to]-ға көбейеді
ans[to] = ans[v]
− жақындаған вершина саны
+ алыстаған вершина саны

Екінші DFS

Екінші DFS ішінде барлық вершина үшін ans санаймыз.

void dfs2(int v, int p) {
    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        ans[to] = ans[v] - sz[to] + (n - sz[to]);

        dfs2(to, v);
    }
}

Біз v үшін answer білеміз. Сол арқылы to үшін answer аламыз.

Толық реализация

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n;
vector<int> g[N];
long long sz[N];
long long dp[N];
long long ans[N];

void dfs1(int v, int p) {
    sz[v] = 1;
    dp[v] = 0;

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        dfs1(to, v);

        sz[v] += sz[to];
        dp[v] += dp[to] + sz[to];
    }
}

void dfs2(int v, int p) {
    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        ans[to] = ans[v] - sz[to] + (n - sz[to]);

        dfs2(to, v);
    }
}

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);
    }

    dfs1(1, 0);

    ans[1] = dp[1];

    dfs2(1, 0);

    for (int i = 1; i <= n; i++) {
        cout << ans[i] << ' ';
    }

    return 0;
}

Input мысалы

Input

5
1 2
1 3
3 4
3 5

Output

6 9 5 8 8

Мұнда:

ans[1] = 6
ans[2] = 9
ans[3] = 5
ans[4] = 8
ans[5] = 8

ans[3]-ті тексерейік

3 вершинасынан distance-тар:

dist(3,1) = 1
dist(3,2) = 2
dist(3,3) = 0
dist(3,4) = 1
dist(3,5) = 1

Сумма:

1 + 2 + 0 + 1 + 1 = 5

Сондықтан:

ans[3] = 5

Формула мысалда қалай жұмыс істейді?

Root-ты 1-ден 3-ке ауыстырамыз.

3 вершинасының поддеревосы:

3, 4, 5
sz[3] = 3

Формула:

ans[3] = ans[1] - sz[3] + (n - sz[3]);

Қоямыз:

ans[3] = 6 - 3 + (5 - 3) = 5

Rerooting жалпы схемасы

Көбіне rerooting былай жасалады:

1. Бірінші DFS root 1 үшін dp санайды
2. 1 вершинасы үшін answer аламыз
3. Екінші DFS answer-ды parent-тен child-қа өткізеді
4. Әр child үшін пересчет формула қолданамыз

Rerooting шаблон

void dfs1(int v, int p) {
    // dp[v] төменнен жоғары саналады

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        dfs1(to, v);

        // dp[v] dp[to] арқылы жаңарады
    }
}

void dfs2(int v, int p) {
    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        // ans[to] ans[v] арқылы пересчет болады

        dfs2(to, v);
    }
}

Тағы қандай задачаларда қолданылады?

Rerooting тек distance sum үшін ғана емес. Мысалы:

  • әр вершинадан максималды distance
  • әр вершина root болса, subtree answer
  • path саны
  • поддерево және поддерево сыртындағы мәндер
  • барлық вершина үшін answer
Мағынасы бірдей: алдымен төменнен жоғары санаймыз, кейін answer-ды жоғарыдан төмен өткіземіз.

Задача 2: әр вершинадан ең алыс distance

Әр вершина үшін ең алыс вершинаға дейінгі distance табу керек.

Яғни:

ans[v] = max distance from v to any other vertex

Мұнда сақтаймыз:

  • down[v] — v-дан төмен қарай ең үлкен depth
  • up[v] — v-дан жоғары немесе басқа поддеревоға кететін ең жақсы path

down[v]

down[v] обычный DFS арқылы саналады:

down[v] = max(down[to] + 1);

Егер child жоқ болса:

down[v] = 0;

up[to]

up[to] санау үшін v-дан шығатын, бірақ to поддеревосына кірмейтін ең жақсы жолды білу керек.

Сондықтан әр вершина үшін child-тардан келетін екі max depth сақтаймыз:

best1
best2

Егер child to best1 берген болса, онда оған best2 қолданамыз. Әйтпесе best1 қолданамыз.

Код: әр вершинадан максималды distance

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n;
vector<int> g[N];
int downDist[N];
int upDist[N];
int ans[N];

void dfs1(int v, int p) {
    downDist[v] = 0;

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        dfs1(to, v);

        downDist[v] = max(downDist[v], downDist[to] + 1);
    }
}

void dfs2(int v, int p) {
    int best1 = -1;
    int best2 = -1;

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        int cur = downDist[to] + 1;

        if (cur > best1) {
            best2 = best1;
            best1 = cur;
        } else if (cur > best2) {
            best2 = cur;
        }
    }

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        int use = best1;

        if (downDist[to] + 1 == best1) {
            use = best2;
        }

        upDist[to] = upDist[v] + 1;

        if (use != -1) {
            upDist[to] = max(upDist[to], use + 1);
        }

        dfs2(to, v);
    }
}

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);
    }

    dfs1(1, 0);

    upDist[1] = 0;

    dfs2(1, 0);

    for (int i = 1; i <= n; i++) {
        ans[i] = max(downDist[i], upDist[i]);
        cout << ans[i] << ' ';
    }

    return 0;
}

Rerooting қашан қолданамыз?

Задачада мына сөздер болса, rerooting керек болуы мүмкін:

  • әр вершина үшін
  • егер осы вершина root болса
  • барлық вершина үшін answer
  • әр вершинадан distance суммасы
  • әр вершинадан максималды distance
  • answer поддеревоға және поддерево сыртындағы бөлікке байланысты

Типтік қателер

  • sz[v] санауды ұмыту
  • sz[to] және n - sz[to] шатастыру
  • тек бірінші DFS жасап қою
  • ans[1] = dp[1] қоюды ұмыту
  • parent-қа қайта кіріп кету
  • қай вершина жақындап, қай вершина алыстағанын түсінбей формула жазу
  • rerooting әрқашан бір формула деп ойлау

Асимптотика

Метод Уақыт Память
Әр вершинадан DFS O(n²) O(n)
Rerooting DP O(n) O(n)
Rerooting көбіне O(n) жұмыс істейді, себебі әр DFS ағашты бір рет өтеді.

Distance sum қысқа шаблон

void dfs1(int v, int p) {
    sz[v] = 1;

    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        dfs1(to, v);

        sz[v] += sz[to];
        dp[v] += dp[to] + sz[to];
    }
}

void dfs2(int v, int p) {
    for (int to : g[v]) {
        if (to == p) {
            continue;
        }

        ans[to] = ans[v] - sz[to] + (n - sz[to]);

        dfs2(to, v);
    }
}

Ең бастысы

rerooting = барлық root үшін Tree DP
бірінші DFS төменнен жоғары санайды
екінші DFS answer-ды parent-тен child-қа өткізеді
формуланы задачаның мағынасы бойынша шығару керек