[0]zero-index clytie0x/zero-index

26.6 แบบฝึกหัด

เขียนแต่ละข้อเป็นไฟล์ใหม่ใน Code::Blocks รันแล้วเทียบกับผลลัพธ์ที่ต้องได้ให้ตรงทุกตัวอักษร รวมถึงช่องว่างและการขึ้นบรรทัด ติดจริงๆ ค่อยเปิดคำใบ้ และเปิดเฉลยหลังทำเสร็จแล้วเท่านั้น

ข้อ 1 สมาชิกที่ไม่มีลูกทีม

ระดับง่าย เวลาไม่เกิน 1 วินาที

ชมรมดาราศาสตร์มีสมาชิก N คน หมายเลข 1 ถึง N ทุกคนยกเว้นประธานมีหัวหน้าทีมโดยตรงหนึ่งคน ผังของชมรมจึงเป็นต้นไม้ที่มีประธานเป็นราก ประธานไม่จำเป็นต้องเป็นคนหมายเลข 1

ชมรมอยากรู้สองอย่าง อย่างแรก มีสมาชิกกี่คนที่ไม่มีลูกทีมเลย คือไม่ได้เป็นหัวหน้าโดยตรงของใคร อย่างที่สอง ผังมีกี่ชั้น นับเป็นจำนวนทอดจากประธานลงไปถึงสมาชิกที่อยู่ลึกที่สุด ประธานอยู่ลึก 0 ลูกทีมโดยตรงของประธานอยู่ลึก 1 ไปเรื่อยๆ

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม N โดยที่ 1 ≤ N ≤ 1000

บรรทัดที่สองมีจำนวนเต็ม N ตัว ตัวที่ i คือหมายเลขหัวหน้าโดยตรงของสมาชิกคนที่ i ประธานมีค่านี้เป็น 0 มีประธานคนเดียวพอดี และข้อมูลเป็นต้นไม้เสมอ

ข้อมูลออก

บรรทัดแรกพิมพ์จำนวนสมาชิกที่ไม่มีลูกทีม บรรทัดที่สองพิมพ์ความลึกที่มากที่สุด

9
0 1 1 2 2 3 1 5 7
4
3
5
2 0 2 3 3
3
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 เกมทายสัตว์

ระดับง่าย เวลาไม่เกิน 1 วินาที

เกมทายสัตว์เก็บคำถามไว้เป็น 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 5
3 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 วินาที

หมู่บ้านแห่งหนึ่งทำแผนผังเครือญาติย้อนไปถึงต้นตระกูลคนเดียว ทุกคนในแผนผังมีหมายเลข 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 7
1 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 ยอดขายของทีม

ระดับง่าย เวลาไม่เกิน 1 วินาที

บริษัทขายตรงมีพนักงาน 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 7
122 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 จุดชมวิวที่สูงกว่าทุกจุดก่อนหน้า

ระดับกลาง เวลาไม่เกิน 1 วินาที

อุทยานแห่งชาติมีจุดชมวิว 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 80
4
ดูคำใบ้

จุดหนึ่งดีเด่นเมื่อสูงกว่าจุดที่สูงที่สุดบนทางจากทางเข้ามาถึงจุดก่อนหน้า ให้เก็บ 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 ฮีปทีละขั้น

ระดับง่าย เวลาไม่เกิน 1 วินาที

ครูให้นักเรียนฝึกเขียน 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
-
-
+ 55
80 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 รหัสหนังสือในต้นไม้ค้นหา

ระดับกลาง เวลาไม่เกิน 1 วินาที

ห้องสมุดเก็บรหัสหนังสือไว้ใน 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
? 180
2
-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 เส้นทางเดินป่าที่ไกลที่สุด

ระดับยาก เวลาไม่เกิน 1 วินาที

อุทยานแห่งหนึ่งมีจุดพัก 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 6
24
ดูคำใบ้

ทำแบบเส้นผ่านศูนย์กลางในหน้า 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 ความลึกตอนใส่ลงต้นไม้ค้นหา

ระดับยาก เวลาไม่เกิน 1 วินาที

ระบบลงทะเบียนวิ่งมาราธอนเก่าเครื่องหนึ่งเก็บหมายเลขนักวิ่งไว้ใน 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 180
0 1 1 2 2 2 2
5
10 20 30 25 5
0 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 งานเลี้ยงชมรม

ระดับยาก เวลาไม่เกิน 1 วินาที

ชมรมหุ่นยนต์มีสมาชิก 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 6
19
ดูคำใบ้

คิดแบบ 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;
}