26.6 แบบฝึกหัด
เขียนแต่ละข้อเป็นไฟล์ใหม่ใน Code::Blocks รันแล้วเทียบกับผลลัพธ์ที่ต้องได้ให้ตรงทุกตัวอักษร รวมถึงช่องว่างและการขึ้นบรรทัด ติดจริงๆ ค่อยเปิดคำใบ้ และเปิดเฉลยหลังทำเสร็จแล้วเท่านั้น
ข้อ 1 สมาชิกที่ไม่มีลูกทีม
ชมรมดาราศาสตร์มีสมาชิก N คน หมายเลข 1 ถึง N ทุกคนยกเว้นประธานมีหัวหน้าทีมโดยตรงหนึ่งคน ผังของชมรมจึงเป็นต้นไม้ที่มีประธานเป็นราก ประธานไม่จำเป็นต้องเป็นคนหมายเลข 1
ชมรมอยากรู้สองอย่าง อย่างแรก มีสมาชิกกี่คนที่ไม่มีลูกทีมเลย คือไม่ได้เป็นหัวหน้าโดยตรงของใคร อย่างที่สอง ผังมีกี่ชั้น นับเป็นจำนวนทอดจากประธานลงไปถึงสมาชิกที่อยู่ลึกที่สุด ประธานอยู่ลึก 0 ลูกทีมโดยตรงของประธานอยู่ลึก 1 ไปเรื่อยๆ
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 1000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขหัวหน้าโดยตรงของสมาชิกคนที่ i ประธานมีค่านี้เป็น 0 มีประธานคนเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
ข้อมูลออก
บรรทัดแรกพิมพ์จำนวนสมาชิกที่ไม่มีลูกทีม บรรทัดที่สองพิมพ์ความลึกที่มากที่สุด
9
0 1 1 2 2 3 1 5 74
35
2 0 2 3 33
2ดูคำใบ้
อ่านพ่อของทุกคนเก็บไว้ในอาเรย์ คนที่ไม่มีใครมีเขาเป็นพ่อคือใบ นับได้ด้วยอาเรย์ที่นับว่าแต่ละคนเป็นพ่อของกี่คน ความลึกของแต่ละคนหาได้ด้วยการเดินขึ้นหาประธานแบบหน้า 26.1 N ไม่เกินพัน จึงเดินขึ้นจากทุกคนได้สบาย ระวังกรณีชมรมมีคนเดียว ประธานเป็นใบด้วย และความสูงเป็น 0
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> parent(n + 1), child_count(n + 1, 0);
for (int v = 1; v <= n; v++) {
cin >> parent[v];
if (parent[v] != 0) {
child_count[parent[v]]++;
}
}
int leaves = 0, height = 0;
for (int v = 1; v <= n; v++) {
if (child_count[v] == 0) {
leaves++;
}
int depth = 0;
for (int u = v; parent[u] != 0; u = parent[u]) {
depth++;
}
height = max(height, depth);
}
cout << leaves << '\n' << height << '\n';
return 0;
}ข้อ 2 เกมทายสัตว์
เกมทายสัตว์เก็บคำถามไว้เป็น binary tree มี N โหนด หมายเลข 1 ถึง N แต่ละโหนดมีลูกซ้าย (ตอบใช่) และลูกขวา (ตอบไม่ใช่) ได้อย่างละไม่เกินหนึ่ง ผู้พัฒนาเกมอยากตรวจว่าเก็บต้นไม้ถูกไหม จึงอยากได้หมายเลขโหนดเรียงตามลำดับ preorder inorder และ postorder
ข้อมูลเข้าไม่ได้บอกว่าโหนดไหนเป็นราก รากคือโหนดเดียวที่ไม่ได้เป็นลูกของโหนดใด
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 1000
อีก N บรรทัด บรรทัดที่ v มีจำนวนเต็มสองตัว คือหมายเลขลูกซ้ายและลูกขวาของโหนด v ถ้าไม่มีลูกทางนั้นจะเป็น 0 ข้อมูลเป็น binary tree เสมอ
ข้อมูลออก
สามบรรทัด บรรทัดแรกคือหมายเลขโหนดตามลำดับ preorder บรรทัดที่สองตามลำดับ inorder บรรทัดที่สามตามลำดับ postorder แต่ละบรรทัดคั่นหมายเลขด้วยช่องว่างหนึ่งช่อง
6
0 4
0 0
1 6
0 0
0 0
2 53 1 4 6 2 5
1 4 3 2 6 5
4 1 2 5 6 3ดูคำใบ้
เก็บลูกซ้ายลูกขวาในอาเรย์แบบหน้า 26.1 แล้วหารากจากโหนดที่ไม่มีใครชี้มาหา จากนั้นเขียนฟังก์ชันท่องสามแบบตามหน้า 26.2 อย่าลืมกรณีฐานของโหนด 0 เก็บหมายเลขที่ไปถึงลง vector ก่อนแล้วค่อยพิมพ์ จะได้ไม่มีช่องว่างเกินท้ายบรรทัด ต้นไม้ลึกได้ไม่เกินพันชั้น recursion จึงไม่มีปัญหา
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
vector<int> left_child, right_child, visited;
void preorder(int v)
{
if (v == 0) {
return;
}
visited.push_back(v);
preorder(left_child[v]);
preorder(right_child[v]);
}
void inorder(int v)
{
if (v == 0) {
return;
}
inorder(left_child[v]);
visited.push_back(v);
inorder(right_child[v]);
}
void postorder(int v)
{
if (v == 0) {
return;
}
postorder(left_child[v]);
postorder(right_child[v]);
visited.push_back(v);
}
void print_and_clear()
{
for (int i = 0; i < (int)visited.size(); i++) {
if (i > 0) {
cout << ' ';
}
cout << visited[i];
}
cout << '\n';
visited.clear();
}
int main()
{
int n;
cin >> n;
left_child.assign(n + 1, 0);
right_child.assign(n + 1, 0);
vector<bool> has_parent(n + 1, false);
for (int v = 1; v <= n; v++) {
cin >> left_child[v] >> right_child[v];
has_parent[left_child[v]] = true;
has_parent[right_child[v]] = true;
}
int root = 0;
for (int v = 1; v <= n; v++) {
if (!has_parent[v]) {
root = v;
}
}
preorder(root);
print_and_clear();
inorder(root);
print_and_clear();
postorder(root);
print_and_clear();
return 0;
}ข้อ 3 นับคนแต่ละรุ่น
หมู่บ้านแห่งหนึ่งทำแผนผังเครือญาติย้อนไปถึงต้นตระกูลคนเดียว ทุกคนในแผนผังมีหมายเลข 1 ถึง N และทุกคนยกเว้นต้นตระกูลมีพ่อหรือแม่ในแผนผังหนึ่งคน ต้นตระกูลเป็นรุ่นที่ 0 ลูกของต้นตระกูลเป็นรุ่นที่ 1 หลานเป็นรุ่นที่ 2 ไปเรื่อยๆ ให้นับว่าแต่ละรุ่นมีกี่คน
แผนผังบางสายสืบต่อกันยาวมาก จำนวนรุ่นจึงอาจมากถึง N
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขพ่อหรือแม่ของคนที่ i ต้นตระกูลมีค่านี้เป็น 0 มีต้นตระกูลคนเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
ข้อมูลออก
บรรทัดเดียว ถ้ารุ่นที่ลึกที่สุดคือรุ่นที่ H ให้พิมพ์จำนวนเต็ม H + 1 ตัว ตัวที่ k (เริ่มนับที่ 0) คือจำนวนคนในรุ่นที่ k คั่นด้วยช่องว่างหนึ่งช่อง
9
0 1 1 2 2 3 1 5 71 3 4 1ดูคำใบ้
สร้างรายชื่อลูกจากพ่อของแต่ละคนแบบหน้า 26.1 แล้วท่องทีละชั้นด้วย queue แบบหน้า 26.2 ได้ความลึกของทุกคนใน O(N) แล้วนับว่าแต่ละความลึกมีกี่คน การเดินขึ้นหาต้นตระกูลจากทุกคนเป็น O(N2) เมื่อแผนผังเป็นสายตรงยาว และ recursion ที่ลึกเท่าความสูงจะพังกับสายตรงสองแสนรุ่นตามหน้า 26.3
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<vector<int>> children(n + 1);
int root = 0;
for (int v = 1; v <= n; v++) {
int p;
cin >> p;
if (p == 0) {
root = v;
} else {
children[p].push_back(v);
}
}
vector<int> depth(n + 1, 0), count_at;
queue<int> waiting;
waiting.push(root);
while (!waiting.empty()) {
int v = waiting.front();
waiting.pop();
if (depth[v] == (int)count_at.size()) {
count_at.push_back(0);
}
count_at[depth[v]]++;
for (int c : children[v]) {
depth[c] = depth[v] + 1;
waiting.push(c);
}
}
for (int k = 0; k < (int)count_at.size(); k++) {
if (k > 0) {
cout << ' ';
}
cout << count_at[k];
}
cout << '\n';
return 0;
}ข้อ 4 ยอดขายของทีม
บริษัทขายตรงมีพนักงาน N คน หมายเลข 1 ถึง N ทุกคนยกเว้นผู้จัดการใหญ่มีหัวหน้าโดยตรงหนึ่งคน ผังของบริษัทจึงเป็นต้นไม้ เดือนนี้พนักงานคนที่ i ขายได้ ai บาท
ทีมของพนักงาน v คือ v เองกับทุกคนที่อยู่ใต้ v ลงไปในผัง ไม่ว่ากี่ชั้น ยอดขายของทีม v คือผลรวมยอดขายของทุกคนในทีม ให้หายอดขายของทีมของพนักงานทุกคน
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขหัวหน้าโดยตรงของคนที่ i ผู้จัดการใหญ่มีค่านี้เป็น 0 มีผู้จัดการใหญ่คนเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
บรรทัดที่สามมีจำนวนเต็ม N ตัว คือ a1 ถึง aN โดยที่ 0 ≤ ai ≤ 109
ข้อมูลออก
บรรทัดเดียว มีจำนวนเต็ม N ตัว ตัวที่ i คือยอดขายของทีมของพนักงานคนที่ i คั่นด้วยช่องว่างหนึ่งช่อง
6
0 1 1 2 2 3
50 20 30 10 5 7122 35 37 10 5 7ดูคำใบ้
ยอดของทีม v คือยอดของ v บวกยอดของทีมลูกทีมทุกคน ต้องรู้คำตอบของลูกก่อนพ่อ ใช้วิธีในหน้า 26.3 เก็บลำดับที่หัวหน้ามาก่อนลูกทีมด้วย stack แล้วเดินย้อนจากท้าย บวกยอดของ v ขึ้นไปที่หัวหน้าของ v ยอดรวมถึง 2 × 1014 ต้องใช้ long long ถ้าบวกยอดของแต่ละคนขึ้นไปทุกชั้นจนถึงผู้จัดการ จะเป็น O(N2) เมื่อสายบังคับบัญชายาว และ recursion ที่ลึกเท่าสายบังคับบัญชาจะพัง
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> boss(n + 1);
vector<vector<int>> team(n + 1);
int root = 0;
for (int v = 1; v <= n; v++) {
cin >> boss[v];
if (boss[v] == 0) {
root = v;
} else {
team[boss[v]].push_back(v);
}
}
vector<long long> total(n + 1);
for (int v = 1; v <= n; v++) {
cin >> total[v];
}
vector<int> order;
stack<int> waiting;
waiting.push(root);
while (!waiting.empty()) {
int v = waiting.top();
waiting.pop();
order.push_back(v);
for (int c : team[v]) {
waiting.push(c);
}
}
for (int i = n - 1; i >= 1; i--) {
int v = order[i];
total[boss[v]] += total[v];
}
for (int v = 1; v <= n; v++) {
if (v > 1) {
cout << ' ';
}
cout << total[v];
}
cout << '\n';
return 0;
}ข้อ 5 จุดชมวิวที่สูงกว่าทุกจุดก่อนหน้า
อุทยานแห่งชาติมีจุดชมวิว N จุด หมายเลข 1 ถึง N เชื่อมกันด้วยทางเดินเป็นต้นไม้ จุดหนึ่งเป็นทางเข้าอุทยาน ทุกจุดอื่นมีจุดก่อนหน้าหนึ่งจุดซึ่งอยู่ใกล้ทางเข้ากว่าหนึ่งช่วงทางเดิน จุดที่ i สูงจากระดับน้ำทะเล hi เมตร
นักท่องเที่ยวเดินจากทางเข้าไปยังจุดใดจุดหนึ่งตามทางเดียวที่มี อุทยานจะติดป้ายจุดชมวิวดีเด่นให้จุด v ถ้าจุด v สูงกว่าทุกจุดบนทางจากทางเข้ามาถึง v (ไม่นับ v เอง) อย่างเคร่งครัด ทางเข้าได้ป้ายเสมอ ให้นับว่าต้องทำป้ายกี่ป้าย
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขจุดก่อนหน้าของจุดที่ i ทางเข้ามีค่านี้เป็น 0 มีทางเข้าจุดเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
บรรทัดที่สามมีจำนวนเต็ม N ตัว คือ h1 ถึง hN โดยที่ 1 ≤ hi ≤ 109
ข้อมูลออก
จำนวนจุดชมวิวดีเด่น
7
0 1 1 2 2 3 6
50 70 40 60 90 45 804ดูคำใบ้
จุดหนึ่งดีเด่นเมื่อสูงกว่าจุดที่สูงที่สุดบนทางจากทางเข้ามาถึงจุดก่อนหน้า ให้เก็บ best[v] คือความสูงมากสุดบนทางจากทางเข้ามาถึง v รวม v ด้วย แล้ว best[v] คือค่ามากกว่าระหว่าง best[พ่อของ v] กับความสูงของ v ต้องรู้ค่าของพ่อก่อนลูก จึงเดินตามลำดับที่พ่อมาก่อนลูก จะใช้ queue แบบหน้า 26.2 หรือ stack แบบหน้า 26.3 ก็ได้ การเดินย้อนขึ้นไปเทียบกับทุกจุดก่อนหน้าเป็น O(N2) เมื่อทางเดินยาวเป็นเส้นเดียว
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> parent(n + 1);
vector<vector<int>> children(n + 1);
int root = 0;
for (int v = 1; v <= n; v++) {
cin >> parent[v];
if (parent[v] == 0) {
root = v;
} else {
children[parent[v]].push_back(v);
}
}
vector<int> height(n + 1);
for (int v = 1; v <= n; v++) {
cin >> height[v];
}
vector<int> best(n + 1, 0);
int peaks = 0;
stack<int> waiting;
waiting.push(root);
while (!waiting.empty()) {
int v = waiting.top();
waiting.pop();
int before = (v == root) ? 0 : best[parent[v]];
if (height[v] > before) {
peaks++;
}
best[v] = max(before, height[v]);
for (int c : children[v]) {
waiting.push(c);
}
}
cout << peaks << '\n';
return 0;
}ข้อ 6 ฮีปทีละขั้น
ครูให้นักเรียนฝึกเขียน max-heap ในอาเรย์ตามหน้า 26.5 แล้วอยากได้โปรแกรมเฉลยที่พิมพ์อาเรย์ออกมาให้เทียบทีละขั้น heap เก็บในอาเรย์ช่อง 1 ถึง K เมื่อ K คือจำนวนค่าในฮีป ลูกของช่อง i คือช่อง 2i และ 2i + 1 พ่อของช่อง i คือช่อง i / 2 ปัดเศษทิ้ง โปรแกรมต้องทำตามกติกานี้ทุกตัวอักษร
- สร้างฮีป ใส่ค่าเริ่มต้น N ค่าลงช่อง 1 ถึง N ตามลำดับ แล้วจมลงทีละช่อง ตั้งแต่ช่อง N / 2 (ปัดเศษทิ้ง) ย้อนลงมาจนถึงช่อง 1
- จมลงที่ช่อง i ถ้าช่อง i ไม่มีลูกให้หยุด ไม่งั้นเลือกลูกซ้าย แต่ถ้ามีลูกขวาและลูกขวามากกว่าลูกซ้ายอย่างเคร่งครัดให้เลือกลูกขวาแทน ถ้าค่าที่ช่อง i น้อยกว่าลูกที่เลือกอย่างเคร่งครัด ให้สลับสองช่องนี้แล้วจมลงต่อที่ช่องของลูก ไม่งั้นหยุด
- ใส่ x ต่อ x ไว้ที่ช่อง K + 1 แล้วลอยขึ้น คือถ้ายังไม่ใช่ช่อง 1 และพ่อน้อยกว่าตัวมันอย่างเคร่งครัด ให้สลับกับพ่อแล้วลอยขึ้นต่อ
- หยิบออก จดค่าที่ช่อง 1 ไว้ ย้ายค่าที่ช่อง K มาไว้ช่อง 1 ลดขนาดลงหนึ่ง แล้วจมลงที่ช่อง 1 ถ้าฮีปยังไม่ว่าง
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 100000
บรรทัดที่สองมีจำนวนเต็ม N ตัว คือค่าเริ่มต้นตามลำดับ แต่ละค่าอยู่ระหว่าง 1 ถึง 109
บรรทัดที่สามมีจำนวนเต็ม Q โดยที่ 1 ≤ Q ≤ 100000
อีก Q บรรทัด แต่ละบรรทัดเป็นคำสั่ง + x คือใส่ x (1 ≤ x ≤ 109) หรือ - คือหยิบออก รับประกันว่าไม่มีการหยิบออกตอนที่ฮีปว่าง
ข้อมูลออก
บรรทัดแรกคืออาเรย์หลังสร้างฮีป ช่อง 1 ถึง N คั่นด้วยช่องว่างหนึ่งช่อง
ทุกคำสั่งหยิบออกให้พิมพ์ค่าที่หยิบได้หนึ่งบรรทัด ตามลำดับคำสั่ง
บรรทัดสุดท้ายคืออาเรย์ของฮีปหลังทำทุกคำสั่ง ถ้าฮีปว่างให้พิมพ์ empty
8
30 50 10 80 20 70 60 40
5
-
+ 90
-
-
+ 5580 50 70 40 20 10 60 30
80
90
70
60 50 55 40 20 10 30ดูคำใบ้
เขียน heap เองแบบหน้า 26.5 ใช้อาเรย์เริ่มที่ช่อง 1 สร้างด้วย sift down จากช่อง N / 2 ลงมาถึงช่อง 1 อย่าใส่ทีละตัว เพราะได้อาเรย์คนละแบบ ตอนจมลงให้เลือกลูกขวาเฉพาะเมื่อมากกว่าลูกซ้ายอย่างเคร่งครัด และสลับเฉพาะเมื่อพ่อน้อยกว่าลูกอย่างเคร่งครัด ใช้ priority_queue ไม่ได้ เพราะต้องพิมพ์อาเรย์ข้างใน ทดสอบกับค่าที่ซ้ำกันเยอะๆ ด้วย
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
vector<int> heap = {0};
void sift_up(int i)
{
while (i > 1 && heap[i / 2] < heap[i]) {
swap(heap[i / 2], heap[i]);
i /= 2;
}
}
void sift_down(int i)
{
int n = (int)heap.size() - 1;
while (2 * i <= n) {
int child = 2 * i;
if (child + 1 <= n && heap[child + 1] > heap[child]) {
child++;
}
if (heap[i] >= heap[child]) {
break;
}
swap(heap[i], heap[child]);
i = child;
}
}
void print_heap()
{
if (heap.size() == 1) {
cout << "empty\n";
return;
}
for (int i = 1; i < (int)heap.size(); i++) {
if (i > 1) {
cout << ' ';
}
cout << heap[i];
}
cout << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
heap.resize(n + 1);
for (int i = 1; i <= n; i++) {
cin >> heap[i];
}
for (int i = n / 2; i >= 1; i--) {
sift_down(i);
}
print_heap();
int q;
cin >> q;
for (int k = 0; k < q; k++) {
char op;
cin >> op;
if (op == '+') {
int x;
cin >> x;
heap.push_back(x);
sift_up((int)heap.size() - 1);
} else {
cout << heap[1] << '\n';
heap[1] = heap.back();
heap.pop_back();
if (heap.size() > 1) {
sift_down(1);
}
}
}
print_heap();
return 0;
}ข้อ 7 รหัสหนังสือในต้นไม้ค้นหา
ห้องสมุดเก็บรหัสหนังสือไว้ใน binary search tree ตามหน้า 26.4 เริ่มจากต้นไม้ว่าง แล้วทำคำสั่ง Q คำสั่งตามลำดับ
+ xใส่รหัส x ด้วยการเดินลงจากราก x น้อยกว่าคีย์ของโหนดไปซ้าย มากกว่าไปขวา จนเจอที่ว่าง แล้วแขวนโหนดใหม่ไว้ตรงนั้น รับประกันว่า x ยังไม่มีในต้นไม้- xลบรหัส x ซึ่งรับประกันว่ามีอยู่ ถ้าโหนดนั้นไม่มีลูกให้ลบทิ้ง ถ้ามีลูกตัวเดียวให้ลูกขึ้นมาแทนที่ ถ้ามีลูกสองตัว ให้หาคีย์ที่น้อยที่สุดในต้นไม้ย่อยขวาของมัน ลอกคีย์นั้นมาใส่โหนดนี้ แล้วลบคีย์นั้นออกจากต้นไม้ย่อยขวาด้วยกติกาเดียวกัน? xพิมพ์ความลึกของโหนดที่มีรหัส x รากลึก 0 ถ้าไม่มีรหัสนี้ในต้นไม้ให้พิมพ์ -1
ทำครบทุกคำสั่งแล้ว ให้พิมพ์รหัสทั้งหมดในต้นไม้ตามลำดับ preorder
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม Q โดยที่ 1 ≤ Q ≤ 5000
อีก Q บรรทัด บรรทัดละหนึ่งคำสั่งตามรูปแบบข้างบน โดยที่ 1 ≤ x ≤ 109
ข้อมูลออก
ทุกคำสั่ง ? ให้พิมพ์คำตอบหนึ่งบรรทัดตามลำดับ บรรทัดสุดท้ายคือรหัสตามลำดับ preorder คั่นด้วยช่องว่างหนึ่งช่อง ถ้าต้นไม้ว่างให้พิมพ์ empty
14
+ 165
+ 152
+ 176
+ 149
+ 158
+ 171
+ 180
? 171
? 160
- 149
- 152
- 165
? 171
? 1802
-1
0
2
171 158 176 180ดูคำใบ้
เขียน BST ด้วย node กับ pointer ตามหน้า 26.4 ใส่ด้วยลูปเดินลง ลบด้วยฟังก์ชันที่คืนรากใหม่ของต้นไม้ย่อย กรณีมีลูกสองตัวให้ลอกคีย์ที่น้อยที่สุดในต้นไม้ย่อยขวามาแล้วลบตัวเดิมทิ้ง หาความลึกด้วยการเดินลงจากรากแล้วนับก้าว ใช้ set ไม่ได้ เพราะความลึกขึ้นกับรูปร่างของต้นไม้ คำสั่งมีไม่เกินห้าพัน ต้นไม้เอียงสุดก็ลึกไม่เกินห้าพันชั้น ทำตรงๆ ทันแน่นอน ทดสอบการลบรากที่มีลูกสองตัวด้วย
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
struct Node {
int key;
Node *left;
Node *right;
};
Node *insert(Node *root, int key)
{
Node *node = new Node;
node->key = key;
node->left = nullptr;
node->right = nullptr;
if (root == nullptr) {
return node;
}
Node *cur = root;
while (true) {
if (key < cur->key) {
if (cur->left == nullptr) {
cur->left = node;
break;
}
cur = cur->left;
} else {
if (cur->right == nullptr) {
cur->right = node;
break;
}
cur = cur->right;
}
}
return root;
}
Node *erase(Node *node, int key)
{
if (node == nullptr) {
return nullptr;
}
if (key < node->key) {
node->left = erase(node->left, key);
return node;
}
if (key > node->key) {
node->right = erase(node->right, key);
return node;
}
if (node->left == nullptr) {
Node *child = node->right;
delete node;
return child;
}
if (node->right == nullptr) {
Node *child = node->left;
delete node;
return child;
}
Node *next = node->right;
while (next->left != nullptr) {
next = next->left;
}
node->key = next->key;
node->right = erase(node->right, next->key);
return node;
}
int depth_of(Node *root, int key)
{
int depth = 0;
Node *cur = root;
while (cur != nullptr) {
if (key == cur->key) {
return depth;
}
if (key < cur->key) {
cur = cur->left;
} else {
cur = cur->right;
}
depth++;
}
return -1;
}
void preorder(Node *node, vector<int> &keys)
{
if (node == nullptr) {
return;
}
keys.push_back(node->key);
preorder(node->left, keys);
preorder(node->right, keys);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
Node *root = nullptr;
for (int k = 0; k < q; k++) {
char op;
int x;
cin >> op >> x;
if (op == '+') {
root = insert(root, x);
} else if (op == '-') {
root = erase(root, x);
} else {
cout << depth_of(root, x) << '\n';
}
}
vector<int> keys;
preorder(root, keys);
if (keys.empty()) {
cout << "empty";
}
for (int i = 0; i < (int)keys.size(); i++) {
if (i > 0) {
cout << ' ';
}
cout << keys[i];
}
cout << '\n';
return 0;
}ข้อ 8 เส้นทางเดินป่าที่ไกลที่สุด
อุทยานแห่งหนึ่งมีจุดพัก N จุด หมายเลข 1 ถึง N เชื่อมกันด้วยทางเดินป่า N - 1 เส้น จากจุดหนึ่งไปอีกจุดหนึ่งมีทางเดียวเสมอ ผังทางเดินจึงเป็นต้นไม้ อุทยานเก็บผังไว้โดยเลือกจุดหนึ่งเป็นศูนย์บริการ แล้วบอกว่าจุดอื่นแต่ละจุดต่อกับจุดถัดไปทางศูนย์บริการจุดไหน และทางเดินช่วงนั้นยาวกี่เมตร
อุทยานจะจัดงานวิ่งเทรลจากจุดพักหนึ่งไปอีกจุดพักหนึ่งตามทางเดินที่มี และอยากได้เส้นทางที่ยาวที่สุดเท่าที่เป็นไปได้ จุดเริ่มและจุดจบเป็นจุดไหนก็ได้ ไม่จำเป็นต้องผ่านศูนย์บริการ ให้หาความยาวของเส้นทางนั้น
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขจุดถัดไปทางศูนย์บริการของจุดที่ i ศูนย์บริการมีค่านี้เป็น 0 มีศูนย์บริการจุดเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
บรรทัดที่สามมีจำนวนเต็ม N ตัว ตัวที่ i คือความยาวเป็นเมตรของทางเดินจากจุดที่ i ไปยังจุดถัดไปทางศูนย์บริการ ของศูนย์บริการเป็น 0 ของจุดอื่นอยู่ระหว่าง 1 ถึง 109
ข้อมูลออก
ความยาวของเส้นทางที่ยาวที่สุด ถ้ามีจุดพักจุดเดียวให้ตอบ 0
7
0 1 1 2 2 3 6
0 4 2 7 3 5 624ดูคำใบ้
ทำแบบเส้นผ่านศูนย์กลางในหน้า 26.2 แต่นับระยะทางแทนจำนวนเส้น ให้ down[v] คือระยะทางลงจาก v ไปจุดที่ไกลที่สุดในต้นไม้ย่อยของ v ทางที่มี v เป็นจุดสูงสุดยาวเท่ากับผลรวมของ "ระยะลงผ่านลูก c" สองค่าที่มากที่สุด เมื่อระยะลงผ่านลูก c คือ down[c] บวกความยาวทางจาก c ถึง v ต้องรู้ค่าของลูกก่อนพ่อ ให้ใช้ลำดับที่พ่อมาก่อนลูกแล้วเดินย้อนตามหน้า 26.3 เพราะทางเดินอาจต่อกันเป็นเส้นยาวสองแสนจุด ระยะทางรวมถึง 2 × 1014 ต้องใช้ long long การหาระยะจากทุกจุดไปทุกจุดเป็น O(N2) ไม่ทัน
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> parent(n + 1);
vector<vector<int>> children(n + 1);
int root = 0;
for (int v = 1; v <= n; v++) {
cin >> parent[v];
if (parent[v] == 0) {
root = v;
} else {
children[parent[v]].push_back(v);
}
}
vector<long long> length(n + 1);
for (int v = 1; v <= n; v++) {
cin >> length[v];
}
vector<int> order;
stack<int> waiting;
waiting.push(root);
while (!waiting.empty()) {
int v = waiting.top();
waiting.pop();
order.push_back(v);
for (int c : children[v]) {
waiting.push(c);
}
}
vector<long long> longest(n + 1, 0), second(n + 1, 0);
long long answer = 0;
for (int i = n - 1; i >= 0; i--) {
int v = order[i];
answer = max(answer, longest[v] + second[v]);
int p = parent[v];
if (p == 0) {
continue;
}
long long down = longest[v] + length[v];
if (down > longest[p]) {
second[p] = longest[p];
longest[p] = down;
} else if (down > second[p]) {
second[p] = down;
}
}
cout << answer << '\n';
return 0;
}ข้อ 9 ความลึกตอนใส่ลงต้นไม้ค้นหา
ระบบลงทะเบียนวิ่งมาราธอนเก่าเครื่องหนึ่งเก็บหมายเลขนักวิ่งไว้ใน binary search tree ที่ไม่ปรับสมดุล แบบเดียวกับหน้า 26.4 เริ่มจากต้นไม้ว่าง แล้วใส่หมายเลขนักวิ่ง N คนตามลำดับที่มาลงทะเบียน การใส่ x คือเดินลงจากราก x น้อยกว่าคีย์ของโหนดไปซ้าย มากกว่าไปขวา จนเจอที่ว่าง แล้วแขวนโหนดใหม่ไว้ตรงนั้น หมายเลขไม่ซ้ำกัน
ผู้ดูแลระบบสงสัยว่าทำไมบางวันระบบช้ามาก จึงอยากรู้ว่านักวิ่งแต่ละคนถูกแขวนไว้ที่ความลึกเท่าไร รากลึก 0
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว คือหมายเลขนักวิ่งตามลำดับที่ใส่ ไม่ซ้ำกัน และอยู่ระหว่าง 1 ถึง 109
ข้อมูลออก
บรรทัดเดียว มีจำนวนเต็ม N ตัว ตัวที่ i คือความลึกของนักวิ่งคนที่ i ตอนที่ถูกใส่ คั่นด้วยช่องว่างหนึ่งช่อง
7
165 152 176 149 158 171 1800 1 1 2 2 2 25
10 20 30 25 50 1 2 3 1ดูคำใบ้
จำลอง BST ตรงๆ ใช้ก้าวเท่าความลึก ถ้าคีย์มาเรียงกันจะเป็น O(N2) ตามหน้า 26.4 ไม่ทัน ให้สังเกตว่าตอนใส่ x โหนดใหม่จะไปแขวนอยู่ใต้ตัวก่อนหน้าหรือตัวถัดไปของ x ในบรรดาคีย์ที่ใส่ไปแล้ว คือคีย์ที่มากที่สุดที่น้อยกว่า x กับคีย์ที่น้อยที่สุดที่มากกว่า x ถ้ามีทั้งสองตัว ตัวหนึ่งต้องเป็นบรรพบุรุษของอีกตัว และ x ไปแขวนใต้ตัวที่ลึกกว่า ความลึกของ x จึงเป็นความลึกที่มากกว่าของสองตัวนั้นบวกหนึ่ง หาสองตัวนี้ด้วย lower_bound ของ set ตามหน้า 20.2 แล้วเก็บความลึกของแต่ละคีย์ไว้ใน map
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
set<int> inserted;
map<int, int> depth;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
int d = 0;
auto after = inserted.lower_bound(x);
if (after != inserted.end()) {
d = max(d, depth[*after] + 1);
}
if (after != inserted.begin()) {
d = max(d, depth[*prev(after)] + 1);
}
inserted.insert(x);
depth[x] = d;
if (i > 0) {
cout << ' ';
}
cout << d;
}
cout << '\n';
return 0;
}ข้อ 10 งานเลี้ยงชมรม
ชมรมหุ่นยนต์มีสมาชิก N คน หมายเลข 1 ถึง N ทุกคนยกเว้นประธานมีหัวหน้าทีมโดยตรงหนึ่งคน ผังของชมรมจึงเป็นต้นไม้ ชมรมจะจัดงานเลี้ยงปลายปี และรู้ว่าถ้าเชิญสมาชิกคนที่ i มา งานจะสนุกขึ้น fi คะแนน
ปัญหาคือ ไม่มีใครอยากมางานพร้อมหัวหน้าทีมโดยตรงของตัวเอง ชมรมจึงตั้งกติกาว่า ห้ามเชิญทั้งสมาชิกคนหนึ่งและหัวหน้าทีมโดยตรงของเขาพร้อมกัน ส่วนหัวหน้าของหัวหน้ามาพร้อมกันได้ ให้หาว่าเลือกเชิญได้ความสนุกรวมมากที่สุดเท่าไร จะไม่เชิญใครเลยก็ได้
ข้อมูลเข้า
บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 200000
บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขหัวหน้าทีมโดยตรงของคนที่ i ประธานมีค่านี้เป็น 0 มีประธานคนเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ
บรรทัดที่สามมีจำนวนเต็ม N ตัว คือ f1 ถึง fN โดยที่ 0 ≤ fi ≤ 109
ข้อมูลออก
ความสนุกรวมที่มากที่สุด
7
0 1 1 2 2 3 3
5 9 4 3 3 2 619ดูคำใบ้
คิดแบบ dynamic programming ทีละต้นไม้ย่อย ให้ take[v] คือความสนุกมากที่สุดของต้นไม้ย่อย v เมื่อเชิญ v และ skip[v] คือเมื่อไม่เชิญ v ถ้าเชิญ v ห้ามเชิญลูกทีมโดยตรงทุกคน take[v] จึงเป็นความสนุกของ v บวก skip[c] ของลูกทุกตัว ถ้าไม่เชิญ v ลูกแต่ละตัวเลือกได้อิสระ skip[v] จึงเป็นผลรวมของค่าที่มากกว่าระหว่าง take[c] กับ skip[c] ต้องรู้ค่าของลูกก่อนพ่อ ใช้ลำดับที่พ่อมาก่อนลูกแล้วเดินย้อนตามหน้า 26.3 คำตอบคือค่าที่มากกว่าของสองค่าที่ราก ลองทุกแบบของการเชิญมี 2N แบบ ใช้ได้แค่ N ราวยี่สิบ
ดูเฉลย
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> boss(n + 1);
vector<vector<int>> team(n + 1);
int root = 0;
for (int v = 1; v <= n; v++) {
cin >> boss[v];
if (boss[v] == 0) {
root = v;
} else {
team[boss[v]].push_back(v);
}
}
vector<long long> take(n + 1), skip(n + 1, 0);
for (int v = 1; v <= n; v++) {
cin >> take[v];
}
vector<int> order;
stack<int> waiting;
waiting.push(root);
while (!waiting.empty()) {
int v = waiting.top();
waiting.pop();
order.push_back(v);
for (int c : team[v]) {
waiting.push(c);
}
}
for (int i = n - 1; i >= 1; i--) {
int v = order[i];
take[boss[v]] += skip[v];
skip[boss[v]] += max(take[v], skip[v]);
}
cout << max(take[root], skip[root]) << '\n';
return 0;
}