資料結構 習題講解

講師 王政祺

ds11

簡短題敘

給定四則運算式,計算其答案。

Python 作法

  • 這邊先提供各位 python 的作法,如果真的在考試遇到了記得把語言切換一下

print(eval(input()))

Python 作法

  • 這邊先提供各位 python 的作法,如果真的在考試遇到了記得把語言切換一下

  • C++ 比較笨,所以我們要研究 eval() 是怎麼運作的

print(eval(input()))

和上課例題有何不同?

  • 比上課例題多了乘除,直接照著做會爛掉

和上課例題有何不同?

  • 比上課例題多了乘除,直接照著做會爛掉

  • Why?

和上課例題有何不同?

  • 比上課例題多了乘除,直接照著做會爛掉

  • Why?乘除的優先度比加減法高

和上課例題有何不同?

  • 比上課例題多了乘除,直接照著做會爛掉

  • Why?乘除的優先度比加減法高

  • 遇到 '(' 往回推的時候要另外處理

考慮一個沒有括弧的運算式

  • 因為乘除比加減優先,所以先把乘除的答案算出來!

考慮一個沒有括弧的運算式

  • 因為乘除比加減優先,所以先把乘除的答案算出來!

  • 過程中如果遇到還不想處理的加減符號,就丟入 queue 中

考慮一個沒有括弧的運算式

  • 因為乘除比加減優先,所以先把乘除的答案算出來!

  • 過程中如果遇到還不想處理的加減符號,就丟入 queue 中

  • 乘除的運算都結束以後,再從 queue 慢慢將加減的結果算出來

圖像說明

  • 兩個 queue 分別紀錄符號和數值

  • 一邊從 stack 中 pop,一邊 push 進 queue

11 + 5 * 3 - 6 / 2

圖像說明

  • 11 後面接的是加減運算,所以丟入 queue

+
11

11 + 5 * 3 - 6 / 2

圖像說明

  • 5 後面接的是乘除運算,因此可以直接計算

+
11

11 + 5 * 3 - 6 / 2

圖像說明

  • 15 後面接的是加減運算,所以丟入 queue

+ -
11 15

11 + 15 - 6 / 2

圖像說明

  • 6 後面接的是乘除運算,因此可以直接計算

+ -
11 15

11 + 15 - 6 / 2

圖像說明

  • 這時候開始從 queue 一邊 pop 一邊計算

+ -
11 15 3

11 + 15 - 3

圖像說明

  • 這時候開始從 queue 一邊 pop 一邊計算

+ -
11 15 3

11 + 15 - 3

圖像說明

  • 這時候開始從 queue 一邊 pop 一邊計算

-
26 3

26 - 3

圖像說明

  • 將最後的結果丟回 stack!

23

23

程式碼講解

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200000;

template <typename T>
struct Stack{
	T arr[MAXN];
	int now;
	Stack() : now(0) {}
	T top() {
		return arr[now-1];
	}
	void pop() {
		now--;
	}
	void push(T val) {
		arr[now++] = val;
	}
	T size() {
		return now;
	}
};

template <typename T>
struct Queue{
	T arr[MAXN];
	int head, tail;
	Queue() : head(0), tail(0) {}
	int front() {
		return arr[head];
	}
	void pop() {
		head++;
		if (head == MAXN) head = 0;
	}
	void push(int val) {
		arr[tail++] = val;
		if (tail == MAXN) tail = 0;
	}
	int size() {
		return (tail + MAXN - head) % MAXN;
	}
};

string str;
int num, k=1;
Stack <int> stk_num; // 代表數字的 stack
Stack <char> stk_sym; // 代表運算符號的 stack
Queue <int> que_num; // 代表數字的 queue
Queue <char> que_sym; // 代表運算符號的 queue

void calc() {
	int a = stk_num.top(), b;
	stk_num.pop();
	char op;
	while (stk_sym.size() && stk_sym.top() != ')') {
		b = stk_num.top();
		stk_num.pop();
		op = stk_sym.top();
		stk_sym.pop();
		if (op == '+' || op == '-') { // 遇到加減運算先不處理
			que_sym.push(op);
			que_num.push(a);
			a = b;
		} else if (op == '*') {
			a = a * b;
		} else if (op == '/') {
			a = a / b;
		}
	}
	que_num.push(a);
	// 這邊回來解決加減運算
	a = que_num.front();
	que_num.pop();
	while (que_sym.size()) {
		b = que_num.front();
		que_num.pop();
		op = que_sym.front();
		que_sym.pop();
		if (op == '+') {
			a = a + b;
		} else if (op == '-') {
			a = a - b;
		}
	}
	if (stk_sym.size()) stk_sym.pop(); // 若並非結尾,則將 ')' pop 出來
	stk_num.push(a); // 將運算結果放回 stack 中
}

signed main() {
	cin >> str;
	for (int i = str.size()-1; i >= 0; i--) { // 由後往前!
		if (str[i] >= '0' && str[i] <= '9') {
			num = num + k * (str[i] - '0');
			if (i-1 >= 0 && str[i-1] >= '0' && str[i-1] <= '9') {
				k = k * 10; // 如果下一位還是數字的話
			} else {
				stk_num.push(num);
				num = 0, k = 1;
			}
		} else if (str[i] != '(') {
			stk_sym.push(str[i]); // 遇到正常運算符號就 push 進 stack
		} else if (str[i] == '(') {
			calc(); // 遇到 '(' 就回推
		}
	}
	calc(); // 結尾回推
	cout << stk_num.top() << '\n';
	return 0;
}

ds13

簡短題敘

給定長度為 \(N\) 的序列,問每個長度 \(K\) 連續區間的區間最大值。

(\(N, K \leq 10^6\))

直覺的作法

  • \(\mathcal{O}(NK)\),只要枚舉每段區間尋找最大值就行了。

直覺的作法

  • \(\mathcal{O}(NK)\),只要枚舉每段區間尋找最大值就行了。

  • 但這樣不會過

直覺的作法

  • \(\mathcal{O}(NK)\),只要枚舉每段區間尋找最大值就行了。

  • 但這樣不會過

  • 我們對這題期望的複雜度是 \(\mathcal{O}(N)\)

如果從左掃到右呢?

  • 每次維護一段長度為 \(K\) 之連續區間的答案

如果從左掃到右呢?

  • 每次維護一段長度為 \(K\) 之連續區間的答案

  • 往右挪一格的時候:

如果從左掃到右呢?

  • 每次維護一段長度為 \(K\) 之連續區間的答案

  • 往右挪一格的時候:

    • 右邊新加入一個數字,拿來跟原本取 max

如果從左掃到右呢?

  • 每次維護一段長度為 \(K\) 之連續區間的答案

  • 往右挪一格的時候:

    • 右邊新加入一個數字,拿來跟原本取 max

    • 但左邊要怎麼刪掉?

如果從左掃到右呢?

  • 每次維護一段長度為 \(K\) 之連續區間的答案

  • 往右挪一格的時候:

    • 右邊新加入一個數字,拿來跟原本取 max

    • 但左邊要怎麼刪掉?答案是沒辦法

還記得「單調性」嗎?

  • 回想一下,要怎麼找到每一項左邊第一個比自己小的數值?

還記得「單調性」嗎?

  • 考慮每個數值的作用範圍(有機會被當成最大值的範圍)

這次要維護的是單調遞減!

  • 如果一個數值的右邊出現了比他還要大的數值,那麼該數值將不再可能成為答案。 

這次要維護的是單調遞減!

  • 如果一個數值的右邊出現了比他還要大的數值,那麼該數值將不再可能成為答案。

  • 用 deque 維護一段區間中到該查詢點(也就是右界)為止遞減的數字(每個數字右邊都沒有比他大的數字)

這次要維護的是單調遞減!

  • 如果一個數值的右邊出現了比他還要大的數值,那麼該數值將不再可能成為答案。

  • 用 deque 維護一段區間中到該查詢點(也就是右界)為止遞減的數字(每個數字右邊都沒有比他大的數字)

  • Why deque?

每個數值的作用範圍有額外限制

  • 我們要找的是「一段區間」的最大值

每個數值的作用範圍有額外限制

  • 我們要找的是「一段區間」的最大值

  • 隨時檢查 deque 的 front 有沒有超過範圍限制

    • deque 才能 pop_front()

那答案呢?

  • 每次處理完後,deque 的 front 就是那段區間的答案!

時間複雜度

  • 每個數值恰好只會被加入 deque 、從 deque 中刪除各一次,因此均攤複雜度為 \(\mathcal{O}(N)\)

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000000;

struct Deque{
	int arr[MAXN], head, tail;
	Deque() : head(0), tail(0) {}
	int front() {
		return arr[head];
	}
	int back() {
		if (tail == 0) return arr[MAXN-1];
		return arr[tail-1];
	}
	void pop_front() {
		head++;
		if (head == MAXN) head = 0;
	}
	void pop_back() {
		if (tail == 0) tail = MAXN;
		tail--;
	}
	void push_front(int val) {
		if (head == 0) head = MAXN;
		arr[--head] = val;
	}
	void push_back(int val) {
		arr[tail++] = val;
		if (tail == MAXN) tail = 0;
	}
	int size() {
		return (tail + MAXN - head) % MAXN;
	}
} deq;

int N, K;
int x[MAXN+1];

signed main() {
	ios_base::sync_with_stdio(0), cin.tie(0);
	cin >> N >> K;
	for (int i = 1; i <= N; i++) {
		cin >> x[i];
	}
	// 先處理前 K-1 個
	for (int i = 1; i <= K-1; i++) {
		while (deq.size() && x[i] >= x[deq.back()]) {
			deq.pop_back();
		}
		deq.push_back(i);
	}
	for (int i = K; i <= N; i++) {
		if (deq.size() && i - deq.front() >= K) deq.pop_front();
		// 如果距離和自己超過 K-1 就代表不在這次區間的考慮範圍

		while (deq.size() && x[i] >= x[deq.back()]) {
			deq.pop_back();
		}
		deq.push_back(i);
		cout << x[deq.front()] << " \n"[i==N];
		// 當前的 front() 即是這段區間的最大值
	}
	return 0;
}

ds25

簡短題敘

給定長度為 \(L\) 的線段,接下來有 \(N\) 次操作,每次選定一個位置 \(x_i\) 切下去將涵蓋該位置之線段分為兩段,求每次所切之線段長度的和。

(\(N \leq 2 \cdot 10^5, L \leq 10^7\))

目標

  • 其實就是維護每段連續的線段,每次切的時候要知道切在哪

如何達成

  • 我們可以用 set 裡頭存 pair 這樣的結構來達成目標

如何達成

  • 我們可以用 set 裡頭存 pair 這樣的結構來達成目標

  • 每個 pair 代表的就是一個線段的左右界

如何達成

  • 我們可以用 set 裡頭存 pair 這樣的結構來達成目標

  • 每個 pair 代表的就是一個線段的左右界

  • 每次要切的時候,用 lower_bound 找到第一個左界比自己小的 pair(也就是會切到的線段)

如何達成

  • 我們可以用 set 裡頭存 pair 這樣的結構來達成目標

  • 每個 pair 代表的就是一個線段的左右界

  • 每次要切的時候,用 lower_bound 找到第一個左界比自己小的 pair(也就是會切到的線段)

  • 將長度加入答案後 erase,然後再 insert 切出來的兩段

如何達成

  • 我們可以用 set 裡頭存 pair 這樣的結構來達成目標

  • 每個 pair 代表的就是一個線段的左右界

  • 每次要切的時候,用 lower_bound 找到第一個左界比自己小的 pair(也就是會切到的線段)

  • 將長度加入答案後 erase,然後再 insert 切出來的兩段

  • AC~

#include <bits/stdc++.h>
#define pii pair<int,int>
#define ff first
#define ss second
using namespace std;

const int MAXN = 200000;
const int INF = 1e9;
int N, L;
int x, k;
int arr[MAXN+1];
long long ans;
set <pii> S;

signed main() {
	cin >> N >> L;
	S.insert(pii(0, L));
	for (int i = 1; i <= N; i++) {
		cin >> x >> k;
		arr[k] = x;
	}
	for (int i = 1; i <= N; i++) {
		pii pr = *prev(S.lower_bound(pii(arr[i], INF)));
		ans += pr.ss - pr.ff;
		if (arr[i] == 0 || arr[i] == L) continue;
		S.erase(pr);
		S.insert(pii(pr.ff, arr[i]));
		S.insert(pii(arr[i], pr.ss));
	}
	cout << ans << '\n';
	return 0;
}

The End

APCS Camp 資料結構 題解

By CasperWang

APCS Camp 資料結構 題解

APCS Camp 資料結構

  • 440