Codeforces Round 981 div3 個人題解(A~G)

ExtractStars發表於2024-10-25

Codeforces Round 981 div3 個人題解(A~G)

Dashboard - Codeforces Round 981 (Div. 3) - Codeforces

火車頭

#define _CRT_SECURE_NO_WARNINGS 1

#include <algorithm>
#include <array>
#include <bitset>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <numeric>
#include <queue>
#include <random>
#include <set>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

#define ft first
#define sd second

#define yes cout << "yes\n"
#define no cout << "no\n"

#define Yes cout << "Yes\n"
#define No cout << "No\n"

#define YES cout << "YES\n"
#define NO cout << "NO\n"

#define pb push_back
#define eb emplace_back

#define all(x) x.begin(), x.end()
#define all1(x) x.begin() + 1, x.end()
#define unq_all(x) x.erase(unique(all(x)), x.end())
#define unq_all1(x) x.erase(unique(all1(x)), x.end())
#define sort_all(x) sort(all(x))
#define sort1_all(x) sort(all1(x))
#define reverse_all(x) reverse(all(x))
#define reverse1_all(x) reverse(all1(x))

#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL

#define RED cout << "\033[91m"
#define GREEN cout << "\033[92m"
#define YELLOW cout << "\033[93m"
#define BLUE cout << "\033[94m"
#define MAGENTA cout << "\033[95m"
#define CYAN cout << "\033[96m"
#define RESET cout << "\033[0m"

// 紅色
#define DEBUG1(x)                     \
    RED;                              \
    cout << #x << " : " << x << endl; \
    RESET;

// 綠色
#define DEBUG2(x)                     \
    GREEN;                            \
    cout << #x << " : " << x << endl; \
    RESET;

// 藍色
#define DEBUG3(x)                     \
    BLUE;                             \
    cout << #x << " : " << x << endl; \
    RESET;

// 品紅
#define DEBUG4(x)                     \
    MAGENTA;                          \
    cout << #x << " : " << x << endl; \
    RESET;

// 青色
#define DEBUG5(x)                     \
    CYAN;                             \
    cout << #x << " : " << x << endl; \
    RESET;

// 黃色
#define DEBUG6(x)                     \
    YELLOW;                           \
    cout << #x << " : " << x << endl; \
    RESET;

using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
// typedef __int128_t i128;

typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ld, ld> pdd;
typedef pair<ll, int> pli;
typedef pair<string, string> pss;
typedef pair<string, int> psi;
typedef pair<string, ll> psl;

typedef tuple<int, int, int> ti3;
typedef tuple<ll, ll, ll> tl3;
typedef tuple<ld, ld, ld> tld3;

typedef vector<bool> vb;
typedef vector<int> vi;
typedef vector<ll> vl;
typedef vector<string> vs;
typedef vector<pii> vpii;
typedef vector<pll> vpll;
typedef vector<pli> vpli;
typedef vector<pss> vpss;
typedef vector<ti3> vti3;
typedef vector<tl3> vtl3;
typedef vector<tld3> vtld3;

typedef vector<vi> vvi;
typedef vector<vl> vvl;

typedef queue<int> qi;
typedef queue<ll> ql;
typedef queue<pii> qpii;
typedef queue<pll> qpll;
typedef queue<psi> qpsi;
typedef queue<psl> qpsl;

typedef priority_queue<int> pqi;
typedef priority_queue<ll> pql;
typedef priority_queue<string> pqs;
typedef priority_queue<pii> pqpii;
typedef priority_queue<psi> pqpsi;
typedef priority_queue<pll> pqpll;
typedef priority_queue<psi> pqpsl;

typedef map<int, int> mii;
typedef map<int, bool> mib;
typedef map<ll, ll> mll;
typedef map<ll, bool> mlb;
typedef map<char, int> mci;
typedef map<char, ll> mcl;
typedef map<char, bool> mcb;
typedef map<string, int> msi;
typedef map<string, ll> msl;
typedef map<int, bool> mib;

typedef unordered_map<int, int> umii;
typedef unordered_map<ll, ll> uml;
typedef unordered_map<char, int> umci;
typedef unordered_map<char, ll> umcl;
typedef unordered_map<string, int> umsi;
typedef unordered_map<string, ll> umsl;

std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());

template <typename T>
inline T read()
{
    T x = 0;
    int y = 1;
    char ch = getchar();
    while (ch > '9' || ch < '0')
    {
        if (ch == '-')
            y = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9')
    {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return x * y;
}

template <typename T>
inline void write(T x)
{
    if (x < 0)
    {
        putchar('-');
        x = -x;
    }
    if (x >= 10)
    {
        write(x / 10);
    }
    putchar(x % 10 + '0');
}

/*#####################################BEGIN#####################################*/
void solve()
{
}

int main()
{
    ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
    // freopen("test.in", "r", stdin);
    // freopen("test.out", "w", stdout);
    int _ = 1;
    std::cin >> _;
    while (_--)
    {
        solve();
    }
    return 0;
}

/*######################################END######################################*/
// 連結:

A. Sakurako and Kosuke

時間限制:每個測試 1 秒
記憶體限制:每個測試 256 兆位元組

Sakurako 和 Kosuke 決定用座標線上的一個點來玩一些遊戲。這個點目前位於位置 \(x=0\)。他們將輪流行動,Sakurako 將首先行動。

在第 \(i\) 步,當前玩家將把點向某個方向移動 \(2 \cdot i - 1\) 個單位。Sakurako 將始終向負方向移動點,而 Kosuke 將始終向正方向移動點。

換句話說,將會發生以下情況:

  • Sakurako 現在將點的位置改變 \(-1\)\(x=-1\) 現在
  • Kosuke 現在將點的位置改變 \(3\)\(x=2\) 現在
  • Sakurako 現在將點的位置改變 \(-5\)\(x=-3\) 現在

只要點的座標絕對值不超過 \(n\),他們就會繼續玩。更正式地說,遊戲在 \(-n \leq x \leq n\) 時繼續。可以證明遊戲總會結束。

你的任務是確定誰將是最後一個回合的人。

輸入
第一行包含一個整數 \(t\) (\(1 \leq t \leq 100\)) — Sakurako 和 Kosuke 玩的遊戲數量。

每場遊戲都由一個數字 \(n\) (\(1 \leq n \leq 100\)) 描述 — 該數字定義遊戲結束時的條件。

輸出
對於每個 \(t\) 場遊戲,輸出一行該遊戲的結果。如果 Sakurako 完成了最後一輪,則輸出“Sakurako”(不帶引號);否則輸出“Kosuke”。

示例
輸入

4
1
6
3
98

輸出

Kosuke
Sakurako
Kosuke
Sakurako

解題思路

觀察樣例發現,\(|x_i|=i\),第\(i\)個位置的絕對值為\(i\),所以只需要判斷奇偶即可,奇數一定是Kosuke最後走,偶數一定是Sakurako最後走。

程式碼實現

void solve()
{
    int n;
    cin >> n;
    if (n & 1)
        cout << "Kosuke\n";
    else
        cout << "Sakurako\n";
}

B. Sakurako and Water

時間限制:每個測試 2 秒
記憶體限制:每個測試 256 兆位元組

在與小介的旅途中,櫻子和小介發現了一個山谷,可以用大小為 \(n \times n\) 的矩陣來表示,其中第 \(i\) 行和 \(j\) 列的交點處是一座山,高度為 \(a_{i,j}\)。如果 \(a_{i,j} < 0\),那麼那裡有一個湖。

小介非常怕水,所以櫻子需要幫助他:

用她的魔法,她可以選擇一個正方形的山脈區域,並將該區域主對角線上每座山脈的高度增加一。更正式地說,她可以選擇一個子矩陣,其左上角位於 \((i,j)\),右下角位於 \((p,q)\),這樣 \(p - i = q - j\)。然後,她可以將所有 \(k\)\((i+k)\) 行和 \((j+k)\) 列交叉處的每個元素加一,使得 \(0 \leq k \leq p - i\)

已知山谷中每個點的高度都小於 0。

確定櫻子必須使用魔法的最少次數,以使每個尖峰的高度變為非負值。

輸入
第一行包含一個整數 \(t\) (\(1 \leq t \leq 200\)) — 測試用例的數量。

每個測試用例的描述如下:

每個測試用例的第一行由一個數字 \(n\) (\(1 \leq n \leq 500\)) 組成。
接下來的每一行 \(n\) 都由空格分隔的 \(n\) 個整陣列成,這些整數對應於矩陣 \(a\) (\(-10^5 \leq a_{i,j} \leq 10^5\)) 中尖峰的高度。
保證所有測試用例的 \(n\) 之和不超過 1000。

輸出
對於每個測試用例,輸出櫻子必須使用魔法的最少次數,以使所有湖泊消失。

示例
輸入

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

輸出

0
1
4
19

解題思路

如果要使得總操作次數最少,選取的正方形一定是越大越好,這樣覆蓋的可以修改的主對角線山峰才越多。

所有我們每次選擇一定是選擇可以選的最長主對角線。

如果要使得一整條主對角線上的所有山峰大於等於\(0\),那麼至少需要加上這條主對角上小於\(0\)的最小峰的絕對值。

所以我們只需列舉正方形山谷的所有主對角線,找到每條主對角線的小於\(0\)最小值,把它們的絕對值加起來即可。

下圖為樣例3的所有主對角線(顏色相同的在同一主對角線上)

image-20241025042015512

程式碼實現

void solve()
{
    int n;
    cin >> n;
    vvl a(n, vl(n));
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            cin >> a[i][j];
        }
    }
    ll ans = 0;

    for (int i = 0; i < n; i++)
    {
        i64 mn = a[0][i];
        int x = 0, y = i;
        while (x < n && y < n)
        {
            mn = min(mn, a[x][y]);
            x++;
            y++;
        }
        if (mn < 0)
        {
            // cout << mn << endl;
            ans += mn;
        }
    }

    for (int j = 1; j < n; j++)
    {
        ll mn = a[j][0];
        int x = j, y = 0;
        while (x < n && y < n)
        {
            mn = min(mn, a[x][y]);
            x++;
            y++;
        }
        if (mn < 0)
        {
            // cout << mn << endl;
            ans += mn;
        }
    }
    cout << -ans << endl;
}

C. Sakurako's Field Trip

時間限制:每個測試 2 秒
記憶體限制:每個測試 256 兆位元組

即使在大學裡,學生也需要放鬆。這就是 Sakurako 老師決定去實地考察的原因。眾所周知,所有學生都會排成一排。索引為 \(i\) 的學生有一些感興趣的話題,描述為 \(a_i\)。作為一名老師,您希望儘量減少學生隊伍的干擾。

隊伍的干擾定義為具有相同興趣話題的鄰近人數。換句話說,干擾是索引 \(j\) (\(1 \leq j < n\)) 的數量,例如 \(a_j = a_{j+1}\)

為了做到這一點,您可以選擇索引 \(i\) (\(1 \leq i \leq n\)) 並交換位置 \(i\)\(n - i + 1\) 的學生。您可以執行任意數量的交換。

您的任務是確定透過多次執行上述操作可以實現的最小干擾量。

輸入
第一行包含一個整數 \(t\) (\(1 \leq t \leq 10^4\)) — 測試用例的數量。

每個測試用例用兩行描述。

第一行包含一個整數 \(n\) (\(2 \leq n \leq 10^5\)) — 學生隊伍的長度。
第二行包含 \(n\) 個整數 \(a_i\) (\(1 \leq a_i \leq n\)) — 隊伍中學生感興趣的主題。
保證所有測試用例的 \(n\) 之和不超過 \(2 \cdot 10^5\)

輸出
對於每個測試用例,輸出您可以實現的線路的最小可能干擾。

示例
輸入

9
5
1 1 1 2 3
6
2 1 2 2 1 1
4
1 2 1 1
6
2 1 1 2 2 4
4
2 1 2 3
6
1 2 2 1 2 1
5
4 5 5 1 5
7
1 4 3 5 1 1 3
7
3 1 3 2 2 3 3

輸出

1
2
1
0
0
1
1
0
2

說明
在第一個示例中,需要對 \(i=2\) 進行操作,因此陣列將變為 \([1,2,1,1,3]\),其中粗體元素表示已交換的位置。該陣列的干擾量等於 1。

在第四個示例中,只需對 \(i=3\) 進行操作,因此陣列將變為 \([2,1,2,1,2,4]\)。該陣列的干擾量等於 0。

在第八個示例中,只需對 \(i=3\) 進行操作,因此陣列將變為 \([1,4,1,5,3,1,3]\)。該陣列的干擾量等於 0。

解題思路

對於每個位置,統計一下不交換時的干擾\(pre\)和假如交換後的干擾\(nex\),如果\(nex<pre\),直接進行交換。

程式碼實現

void solve()
{
    int n;
    cin >> n;
    vi a(n + 1);
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    for (int i = 2; i <= n / 2; i++)
    {
        int pre = (a[i] == a[i - 1]) + (a[n - i + 1] == a[n - i + 2]);
        int nex = (a[i] == a[n - i + 2]) + (a[n - i + 1] == a[i - 1]);
        if (nex < pre)
            swap(a[i], a[n - i + 1]);
    }
    int ans = 0;
    for (int i = 2; i <= n; i++)
    {
        if (a[i] == a[i - 1])
            ans++;
    }
    cout << ans << "\n";
}

D. Kousuke's Assignment

時間限制:每個測試 2 秒
記憶體限制:每個測試 256 兆位元組

在和 Sakurako 一起旅行後,Kousuke 非常害怕,因為他忘記了他的程式設計作業。在這次作業中,老師給了他一個包含 \(n\) 個整數的陣列 \(a\),並要求他計算陣列 \(a\) 中不重疊線段的數量,使得每個線段都被認為是美麗的。

如果線段 \([l,r]\) 滿足 \(a_l + a_{l+1} + \cdots + a_{r-1} + a_r = 0\),則該線段被認為是美麗的。

對於固定陣列 \(a\),您的任務是計算不重疊的美麗線段的最大數量。

輸入
輸入的第一行包含數字 \(t\) (\(1 \leq t \leq 10^4\)) — 測試用例的數量。每個測試用例由 2 行組成。

第一行包含一個整數 \(n\) (\(1 \leq n \leq 10^5\)) — 陣列的長度。
第二行包含 \(n\) 個整數 \(a_i\) (\(-10^5 \leq a_i \leq 10^5\)) — 陣列 \(a\) 的元素。
保證所有測試用例的 \(n\) 的總和不超過 \(3 \cdot 10^5\)

輸出
對於每個測試用例,輸出一個整數:不重疊的美麗線段的最大數量。

示例
輸入

3
5
2 1 -3 2 1
7
12 -4 4 43 -3 -5 8
6
0 -4 0 3 0 1

輸出

1
2
3

解題思路

字首和的板子題,對於陣列\(a\),我們計算出它的字首和\(pre\)。對於每個字首和,我們維護上一個下標。

如果遇見相同的字首和,查詢該字首和的上一個下標是否小於上一段線段的結束位置。

如果小於則選擇,否則更新該字首和的上一個下標。

程式碼實現

void solve()
{
    int n;
    cin >> n;
    vl a(n);
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    mll mp;
    ll sum = 0;
    mp[0] = -1;
    int cnt = 0;
    int last = -1;
    for (int i = 0; i < n; i++)
    {
        sum += a[i];
        if (mp.find(sum) != mp.end())
        {
            int p = mp[sum];
            if (p >= last)
            {
                cnt++;
                last = i;
            }
        }
        mp[sum] = i;
    }
    cout << cnt << "\n";
}

E. Sakurako, Kosuke, and the Permutation

時間限制:每個測試 2 秒
記憶體限制:每個測試 256 兆位元組

Sakurako 的考試結束了,她表現非常出色。作為獎勵,她得到了一個排列 \(p\)。Kosuke 並不完全滿意,因為他有一次考試不及格,而且沒有收到禮物。他決定潛入她的房間(多虧了她鎖的密碼)並破壞排列,使其變得簡單。

如果對於每個 \(i\) (\(1 \leq i \leq n\)) 滿足以下條件之一,則排列 \(p\) 被認為是簡單的:

  • \(p_i = i\)
  • \(p_{p_i} = i\)

例如,排列 \([1,2,3,4]\)\([5,2,4,3,1]\)\([2,1]\) 是簡單的,而 \([2,3,1]\)\([5,2,1,4,3]\) 則不是。

在一次操作中,Kosuke 可以選擇索引 \(i,j\) (\(1 \leq i,j \leq n\)) 並交換元素 \(p_i\)\(p_j\)

Sakurako 即將回家。您的任務是計算 Kosuke 需要執行的最少運算元,以使排列變得簡單。

輸入
第一行包含一個整數 \(t\) (\(1 \leq t \leq 10^4\)) — 測試用例的數量。

每個測試用例用兩行描述。

第一行包含一個整數 \(n\) (\(1 \leq n \leq 10^6\)) — 排列 \(p\) 的長度。
第二行包含 \(n\) 個整數 \(p_i\) (\(1 \leq p_i \leq n\)) — 排列 \(p\) 的元素。
保證所有測試用例的 \(n\) 之和不超過 \(10^6\)

輸出
對於每個測試用例,輸出 Kosuke 需要執行的最少運算元,以使排列變得簡單。

示例
輸入

6
5
1 2 3 4 5
5
5 4 3 2 1
5
2 3 4 5 1
4
2 3 4 1
3
1 3 2
7
2 3 1 5 6 7 4

輸出

0
0
2
1
0
2

說明
在第一個和第二個示例中,排列已經很簡單。

在第四個示例中,只需交換 \(p_2\)\(p_4\) 即可。因此,在 1 次操作中,排列將變為 \([2,1,4,3]\)。解題思路

解題思路

我們稱\(i\rightarrow p_i \rightarrow p_{p_i} \rightarrow p_{p_{p_i}}\rightarrow \dots \rightarrow i\)為一個迴圈節,觀察發現,對於一個迴圈節來說,我們只需要操作\(\lfloor \frac{ \text{size}-1}{2} \rfloor\)(size為迴圈節大小)次,就可以把迴圈節拆成size小於2。

如圖

image-20241025045001049

所以我們只需使用並查集縮點,然後列舉所有聯通塊,計算\(\lfloor \frac{ \text{size}-1}{2} \rfloor\)之和即可。

感覺這題的難度遠小於C

程式碼實現

struct DSU
{
    vector<int> f;   // 儲存父節點
    vector<int> siz; // 儲存每個集合的大小

    // 預設建構函式
    DSU() {}

    // 建構函式,初始化並查集
    DSU(int n)
    {
        init(n);
    }

    // 初始化並查集
    void init(int n)
    {
        f.resize(n + 1);             // 調整父節點陣列大小
        iota(f.begin(), f.end(), 0); // 將父節點初始化為自身
        siz.assign(n + 1, 1);        // 每個集合初始大小為 1
    }

    // 查詢操作,返回 x 的根節點
    int find(int x)
    {
        // 路徑壓縮
        while (x != f[x])
        {
            // 將 x 的父節點直接指向其祖父節點
            x = f[x] = f[f[x]];
        }
        return x; // 返回根節點
    }

    // 判斷 x 和 y 是否在同一個集合中
    bool same(int x, int y)
    {
        return find(x) == find(y); // 如果根節點相同,則在同一個集合
    }

    // 合併操作,將 x 和 y 所在的集合合併
    bool merge(int x, int y)
    {
        x = find(x); // 找到 x 的根節點
        y = find(y); // 找到 y 的根節點
        if (x == y)
        {
            return false; // 如果已經在同一集合,返回 false
        }
        // 按秩合併,將較小的集合合併到較大的集合中
        if (siz[x] < siz[y])
            swap(x, y);
        siz[x] += siz[y]; // 更新集合大小
        f[y] = x;         // 將 y 的根節點指向 x
        return true;      // 返回 true 表示合併成功
    }

    // 返回 x 所在集合的大小
    int size(int x)
    {
        return siz[find(x)]; // 返回根節點對應的集合大小
    }
};

void solve()
{
    int n;
    cin >> n;
    DSU dsu(n);
    for (int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        dsu.merge(i, x);
    }
    vb vis(n + 1);
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        int pa = dsu.find(i);
        if (vis[pa])
            continue;
        vis[pa] = true;
        int sz = dsu.size(pa);
        if (sz == 1 || sz == 2)
            continue;
        ans += (sz - 1) / 2;
    }
    cout << ans << endl;
}


F. Kosuke's Sloth

**### F. Kosuke 的懶惰

時間限制:每個測試 1 秒
記憶體限制:每個測試 256 兆位元組

Kosuke 太懶了。他不會給你任何圖例,只會給你任務:

斐波那契數定義如下:

  • $ f(1) = f(2) = 1 $
  • $ f(n) = f(n-1) + f(n-2) $ (當 $ n \geq 3 $ 時)

我們將 $ G(n,k) $ 表示為第 $ n $ 個斐波那契數的索引,該數可被 $ k $ 整除。對於給定的 $ n $ 和 $ k $,計算 $ G(n,k) $。

由於這個數字可能太大,因此將其以模數 $ 10^9 + 7 $ 輸出。

例如:$ G(3,2) = 9 $,因為能被 2 整除的第 3 個斐波那契數是 34。序列為 [1, 1, 2, 3, 5, 8, 13, 21, 34]。

輸入
輸入資料的第一行包含一個整數 $ t $ (\(1 \leq t \leq 10^4\)) — 測試用例的數量。

第一行也是唯一一行包含兩個整數 $ n $ 和 $ k $ (\(1 \leq n \leq 10^{18}\), \(1 \leq k \leq 10^5\))。

保證所有測試用例的 $ k $ 之和不超過 $ 10^6 $。

輸出
對於每個測試用例,輸出唯一的數字:模數為 $ 10^9 + 7 $ 後得到的值 $ G(n,k) $。

示例
輸入

3
3 2
100 1
1000000000000 1377

輸出

9
100
999244007

解題思路

由皮亞諾定理可知,模數為\(k\)的迴圈節長度不會超過\(6k\),所以我們可以暴力列舉找到第一個\(k\)的倍數的位置\(p\),答案即為\(pn\)

時間複雜度複雜度\(O(m)\)

相關證明:

Pisano Period - Shiina_Mashiro - 部落格園

推論:
斐波那契數列取餘是否有規律? - 知乎

程式碼實現

const ll mod = 1e9 + 7;

const int N = 1e6 + 5;
int f[N];
void solve()
{
    ll n;
    cin >> n;
    int m;
    cin >> m;
    f[1] = 1;
    f[2] = 1;
    ll p = 0;
    int i = 3;
    while (1)
    {
        f[i] = (f[i - 1] + f[i - 2]) % m;
        if (!f[i])
        {
            p = i;
            break;
        }
        i++;
    }
    if (m == 1)
        p = 1;
    cout << (n % mod) * (p % mod) % mod << "\n";
}

G. Sakurako and Chefir

時間限制:每個測試 4 秒
記憶體限制:每個測試 256 兆位元組

給定一棵樹,其頂點有 $ n $ 個,根節點為頂點 1。當 Sakurako 帶著她的貓 Chefir 穿過這棵樹時,她分心了,Chefir 跑掉了。

為了幫助 Sakurako,Kosuke 記錄了他的 $ q $ 個猜測。在第 $ i $ 個猜測中,他假設 Chefir 在頂點 $ v_i $ 處迷路了,並且有 $ k_i $ 的體力。

此外,對於每個猜測,Kosuke 假設 Chefir 可以沿邊移動任意次數:

  • 從頂點 $ a $ 到頂點 $ b $,如果 $ a $ 是 $ b $ 的祖先,則耐力不會改變;
  • 從頂點 $ a $ 到頂點 $ b $,如果 $ a $ 不是 $ b $ 的祖先,則 Chefir 的耐力會減少 1;
  • 如果 Chefir 的耐力為 0,則他無法進行第二種型別的移動。

對於每個假設,您的任務是找出 Chefir 從頂點 $ v_i $ 到達最遠頂點的距離,並且具有 $ k_i $ 的耐力。

輸入
第一行包含一個整數 $ t $ (\(1 \leq t \leq 10^4\)) — 測試用例的數量。

每個測試用例描述如下:

第一行包含一個整數 $ n $ (\(2 \leq n \leq 2 \cdot 10^5\)) — 樹中的頂點數量。
接下來的 $ n-1 $ 行包含樹的邊。保證給定的邊形成一棵樹。
下一行由一個整數 $ q $ (\(1 \leq q \leq 2 \cdot 10^5\)) 組成,表示 Kosuke 的猜測次數。
接下來的 $ q $ 行描述了 Kosuke 的猜測,其中有兩個整數 $ v_i \(、\) k_i $ (\(1 \leq v_i \leq n\), \(0 \leq k_i \leq n\))。
保證所有測試用例的 $ n $ 與 $ q $ 之和不超過 $ 2 \cdot 10^5 $。

輸出
對於每個測試用例和每個猜測,輸出 Chefir 從具有 $ k_i $ 耐力的起點 $ v_i $ 到最遠頂點的最大距離。

示例
輸入

3
5
1 2
2 3
3 4
3 5
3
5 1
3 1
2 0
9
8 1
1 7
1 4
7 3
4 9
3 2
1 5
3 6
7
6 0
2 3
6 2
8 2
2 4
9 2
6 3
6
2 1
2 5
2 4
5 6
4 3
3
3 1
1 3
6 5

輸出

2
1
2
0
5
2
4
5
5
5
1
3
4

說明
在第一個示例中:

  • 在第一個查詢中,您可以從頂點 5 到頂點 3(耐力減少 1 並變為 0),然後可以到達頂點 4;
  • 在第二個查詢中,從頂點 3 具有 1 點耐力,您只能到達頂點 2、3、4 和 5;
  • 在第三個查詢中,從頂點 2 具有 0 點耐力,您只能到達頂點 2、3、4 和 5。

解題思路

觀察發現,第一種行動實際上就是沿著樹向下走,第二種行動就是沿著樹向上走。

image-20241025052136570

所以對於一個節點來說,我們一定可以一直使用第一種操作走到這個節點的最深子葉子節點。

對於每一個節點,我們可以使用dfs計算出每個節點的最深子葉子節點到當前節點的距離maxDep

那麼一個節點可以走到的最遠距離即為\(\max\{\text{maxDep}_{u+i}+i\},0\le i \le k\)

image-20241025053551749

考慮暴力的做法,對於每一次詢問,我們對於節點\(v\)向上查詢\(k\)個父節點,找到\(\max\{\text{maxDep}_{u+i}+i\}\)

單次詢問時間複雜多為\(O(k)\),如果資料保證隨機,總時間複雜度為\(O(qlogn)\),但可惜不是。遇到鏈的情況會使得時間複雜度退化到\(O(qk)\),一定會超時,所以考慮最佳化。

我們使用dfs計算出每個節點的深度\(dep\),發現對於在樹的一條鏈上的所有詢問,我們實際上尋找的就是區間\([dep_v,dep_{v-k}]\)的最大\(maxDep\)

所以我們可以把詢問離線出來,對於樹的每一條鏈,構建一下線段樹,儲存區間最大\(maxDep\)

然後dfs遍歷所有節點,如果發現當前節點存在詢問,直接線上段樹中進行查詢。

為了節省空間,我們再遍歷完一條樹的鏈之後,可以在遞迴回溯是重置一下當前節點線上段樹中的值,這樣只需要一棵線段樹即可。

實際複雜度為\(O(nlogn)\)

程式碼實現

// 懶標記線段樹模板類
template <class Info, class Tag>
struct LazySegmentTree
{
    const int n;            // 陣列大小
    std::vector<Info> info; // 儲存線段樹節點的資訊
    std::vector<Tag> tag;   // 儲存懶標記

    // 建構函式
    LazySegmentTree(int n) : n(n), info(4 << std::__lg(n)), tag(4 << std::__lg(n)) {}

    // 用於初始化線段樹的建構函式
    LazySegmentTree(std::vector<Info> init) : LazySegmentTree(init.size())
    {
        std::function<void(int, int, int)> build = [&](int p, int l, int r)
        {
            if (r - l == 1)
            {
                info[p] = init[l]; // 葉子節點賦值
                return;
            }
            int m = (l + r) / 2;    // 中間點
            build(2 * p, l, m);     // 構建左子樹
            build(2 * p + 1, m, r); // 構建右子樹
            pull(p);                // 更新父節點
        };
        build(1, 0, n);
    }

    // 更新父節點
    void pull(int p)
    {
        info[p] = info[2 * p] + info[2 * p + 1]; // 合併左右子樹的資訊
    }

    // 應用標籤到節點
    void apply(int p, const Tag &v)
    {
        info[p].apply(v); // 應用標籤到資訊
        tag[p].apply(v);  // 更新懶標記
    }

    // 推送懶標記到子節點
    void push(int p)
    {
        apply(2 * p, tag[p]);     // 推送到左子樹
        apply(2 * p + 1, tag[p]); // 推送到右子樹
        tag[p] = Tag();           // 清空當前節點的懶標記
    }

    // 修改某個位置的值
    void modify(int p, int l, int r, int x, const Info &v)
    {
        if (r - l == 1)
        {
            info[p] = v; // 更新葉子節點
            return;
        }
        int m = (l + r) / 2; // 中間點
        push(p);             // 推送懶標記
        if (x < m)
        {
            modify(2 * p, l, m, x, v); // 遞迴到左子樹
        }
        else
        {
            modify(2 * p + 1, m, r, x, v); // 遞迴到右子樹
        }
        pull(p); // 更新父節點
    }

    // 對外介面,修改某個位置的值
    void modify(int p, const Info &v)
    {
        modify(1, 0, n, p, v);
    }

    // 區間查詢
    Info rangeQuery(int p, int l, int r, int x, int y)
    {
        if (l >= y || r <= x)
        {
            return Info(); // 範圍不相交,返回預設資訊
        }
        if (l >= x && r <= y)
        {
            return info[p]; // 當前區間完全包含在查詢範圍內
        }
        int m = (l + r) / 2; // 中間點
        push(p);             // 推送懶標記
        return rangeQuery(2 * p, l, m, x, y) + rangeQuery(2 * p + 1, m, r, x, y);
    }

    // 對外介面,區間查詢
    Info rangeQuery(int l, int r)
    {
        return rangeQuery(1, 0, n, l, r);
    }
};

// 標籤結構
struct Tag
{
    int change = -inf; // 增加的值

    // 應用標籤的操作
    void apply(Tag t) &
    {
        change = t.change; // 累加增加的值
    }
};

// 資訊結構
struct Info
{
    int max = -inf;
    void apply(Tag t) &
    {
        if (t.change != -inf)
        {
            max = t.change; // 更新最大值
        }
    }

    Info() {};
    Info(int x) : max(x) {}
};

// 資訊結構的加法運算子過載
Info operator+(Info a, Info b)
{
    Info c;
    c.max = max(a.max, b.max);
    return c;
}

// 查詢結構體
struct Query
{
    int k;
    int id;
    Query(int k_, int id_) : k(k_), id(id_) {}
};

void solve()
{
    int n;
    cin >> n;
    vvi adj(n + 1);
    for (int i = 1; i < n; i++)
    {
        int u, v;
        cin >> u >> v;
        adj[u].pb(v);
        adj[v].pb(u);
    }

    vi dep(n + 1);    // 儲存每個節點的深度
    vi maxDep(n + 1); // 儲存每個節點可以向下走的最大深度

    // 深度優先搜尋 (DFS) 函式,用於計算每個節點的深度和最大深度
    function<void(int, int)> dfs1 = [&](int u, int fa)
    {
        maxDep[u] = 0;
        for (auto v : adj[u])
        {
            if (v == fa) // 如果 v 是父節點,跳過
                continue;
            dep[v] = dep[u] + 1;
            dfs1(v, u);
            maxDep[u] = max(maxDep[u], maxDep[v] + 1);
        }
    };
    dfs1(1, 0);

    int m;
    cin >> m;
    vector<vector<Query>> querys(n + 1);
    vi ans(m);

    for (int i = 0; i < m; i++)
    {
        int v, k;
        cin >> v >> k;
        querys[v].pb({k, i});
    }

    // 建立線段樹
    LazySegmentTree<Info, Tag> seg(n);

    // 處理查詢
    function<void(int, int)> dfs2 = [&](int u, int fa)
    {
        // 處理當前節點 u 的所有查詢
        for (auto &q : querys[u])
        {
            int k = q.k, id = q.id;
            ans[id] = max(ans[id], maxDep[u]); // 更新答案為當前節點的最大深度
            // 透過線段樹查詢從 可到達最淺深度dep[u] - k 到 dep[u] 的最大值,並加上 dep[u]
            ans[id] = max(ans[id], seg.rangeQuery(max(0, dep[u] - k), dep[u]).max + dep[u]);
        }

        // 找到當前節點 u 的子節點中最大和次大子樹深度
        int max1 = 0, max2 = 0;
        for (auto v : adj[u])
        {
            if (v == fa)
                continue;
            if (maxDep[v] + 1 > max1)
            {
                max2 = max1;
                max1 = maxDep[v] + 1;
            }
            else if (maxDep[v] + 1 > max2)
            {
                max2 = maxDep[v] + 1;
            }
        }

        // 遞迴處理每個子節點
        for (auto v : adj[u])
        {
            if (v == fa)
                continue;
            // 如果當前子節點的最大深度 + 1 等於 max1,說明最大子樹深度就是maxDep[v],不能重複選,選次大子樹深度
            int d = maxDep[v] + 1 == max1 ? max2 : max1;
            seg.modify(dep[u], Info(d - dep[u])); // 線上段樹中新增走到u節點可向下走的最大深度
            dfs2(v, u);
            seg.modify(dep[u], -inf); // 回溯時重置線段樹中的值
        }
    };
    dfs2(1, 0);
    for (int i = 0; i < m; i++)
    {
        cout << ans[i] << " \n"[i == m - 1];
    }
}

早知道大號多掉點分了,這場用小號打的。

相關文章