Codeforces Round 981 div3 个人题解(A~G)

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

早知道大号多掉点分了,这场用小号打的。

posted @ 2024-10-25 06:02  ExtractStars  阅读(580)  评论(2编辑  收藏  举报