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的所有主對角線(顏色相同的在同一主對角線上)
程式碼實現
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。
如圖
所以我們只需使用並查集縮點,然後列舉所有聯通塊,計算\(\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。
解題思路
觀察發現,第一種行動實際上就是沿著樹向下走,第二種行動就是沿著樹向上走。
所以對於一個節點來說,我們一定可以一直使用第一種操作走到這個節點的最深子葉子節點。
對於每一個節點,我們可以使用dfs計算出每個節點的最深子葉子節點到當前節點的距離maxDep
那麼一個節點可以走到的最遠距離即為\(\max\{\text{maxDep}_{u+i}+i\},0\le i \le k\)
考慮暴力的做法,對於每一次詢問,我們對於節點\(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];
}
}
早知道大號多掉點分了,這場用小號打的。