Skip to content

Commit 02aac60

Browse files
authored
Merge pull request #159 from Phluenam/add_o59_mar_c1_atleast
Add o59_mar_c1_atleast
2 parents bec9e95 + 7dfb1bf commit 02aac60

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

md/o59_mar_c1_atleast.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
ข้อนี้ให้ลำดับจำนวนเต็ม $N$ $(N \leq 100000)$ ค่า $X[1], X[2], \dots, X[N]$
2+
3+
จากนั้นจะมีคำถาม $M$ $(M \leq 50000)$ คำถามมาในรูปแบบจำนวนเต็มสองค่า $s$และ $t$ และจำนวนจริงหนึ่งค่า $u$ แทนคำถามว่าหากจะลบจำนวนใดๆ ก็ได้จาก $X[s..t] = X[s], X[s+1], \dots, X[t]$ จะต้องลบอย่างน้อยกี่ค่าถึงจะทำให้ค่าเฉลี่ยของจำนวนที่เหลือไม่ต่ำกว่า $u$
4+
5+
## แนวคิด
6+
7+
แต่ละคำถาม $s, t, u$ สามารถมองเป็นอีกแบบว่าถามว่าจะเก็บไว้ได้มากสุดกี่ค่า ซึ่งเทียบเท่ากับคำถามว่าต้องลบอย่างน้อยกี่ค่า
8+
9+
สังเกตว่าเราควรจะเลือกเก็บค่าที่มากกว่าไว้ก่อนเสมอ เพราะการลบค่าที่น้อยกว่าก่อนค่าสูงกว่าย่อมทำให้ค่าเฉลี่ยสูงขั้น
10+
11+
ดังนั้นเราหากเราเรียง $1, 2, \dots, N$ ใหม่เป็น $idx[1], idx[2], \dots, idx[N]$ ให้ $X[idx[1]] \geq X[idx[2]] \geq \dots \geq X[idx[N]]$ ในคำถามใดๆ วิธีเลือกลบที่ดีสุดจะมีค่า $j$ (โดยอาจมีมากกว่าหนึ่งค่าของ $j$ ที่ได้วิธีเลือกที่ดีสุด) ที่ทุกค่า $X[idx[1]], X[idx[2]], \dots, X[idx[j]]$ ที่อยู่ในช่วง $X[s..t]$ ถูกเก็บไว้ ให้ค่าเฉลี่ยนี้เป็น $Average(s, t, j)$ (กรณี $j=0$ จะแทนว่าไม่สามารถเก็บไว้ได้เลยเพราะต่ำกว่าค่า $u$ ที่ต้องการหมด)
12+
13+
สำหรับ $X[s], X[s+1], \dots, X[t]$ ใดๆ เมื่อค่า $j$ ลดลง ค่าเฉลี่ยของจำนวนที่เหลืออยู่นี้ย่อมไม่ลดเพราะจะลบค่าต่ำสุดก่อน ในขณะเดียวกันจำนวนที่เก็บไว้ได้ก็ลดลงเช่นกัน ดังนั้นหากเรามีวิธีทำ Query ค่า $Average(s, t, j)$ เราจะสามารถ Binary Search เพื่อหาว่าจะเก็บไว้ได้มากสุดกี่ตัว (เท่ากับต้องลบน้อยสุดกี่ตัว)
14+
15+
สมมิตว่าเรามี Segment Tree ที่ Update ค่าที่ตำแหน่ง $idx[c]$ จาก 0 เป็น $X[idx[c]]$ สำหรับทุก $1 \leq c \leq j$ แล้ว เราจะสามารถ Query ช่วง $s..t$ หาผลรวม $X[i]$ และหาผลรวมว่า Update ไปกี่ตำแหน่งใน $s..t$ แล้วนำมาหารกันเพื่อให้ได้ $Average(s, t, j)$ ที่ต้องการ แต่การทำแบบนี้สำหรับทุกคำถามจะช้าไปจึงต้องใช้โครงสร้างข้อมูลอื่น
16+
17+
วิธีหนึ่งที่จะรองรับ Query นี้คือการใช้ Persistent Segment Tree ซึ่งสามารถเก็บสถานะของ Segment Tree หลังการ Update ค่า $idx[c]$ ทุกครั้งตั้งแต่ $c=1$ ถึง $c=N$ ใน Persistent Segment Tree การ Query หาค่า $Average(s,t,j)$ เราจะเพียงต้อง Query ในสถานะหลัง Update $idx[j]$ แล้ว
18+
19+
## Persistent Segment Tree
20+
21+
Persistent Segment Tree เป็นโครงสร้างข้อมูล Segment Tree (https://programming.in.th/tasks/1147/solution) ที่เพิ่มคุณสมบัติว่าเป็น Persistent Data Structure นั่นคือโครงสร้างข้อมูลนี้จะไม่ลบสถานะเก่าหลังการ Update ซึ่งทำให้สามารถ Query สถานะเก่าๆ ได้แม้ว่ามีการ Update แล้ว
22+
23+
ใน Persistent Segment นอกจากของที่เก็บในแต่ละ Node แล้ว ยังต้องเก็บดัชนีของลูกขวาและลูกซ้ายเพราะมีการสร้าง Node ใหม่เรื่อยๆ ซึ่งต่างจาก Segment Tree ปกติที่มักให้ลูกซ้ายเป็น `n*2` และขวาเป็น `n*2+1`
24+
25+
สำหรับข้อนี้ค่า $S$ ที่ถูกเก็บจะต้องมีทั้งผลรวม $X[l..r]$ ของช่วงและจำนวนค่าที่ถูก Update แล้วเพื่อใช้เป็นตัวส่วนในการคำนวณค่าเฉลี่ย จึงสามารถใช้ `std::pair` โดยให้เก็บค่าเหล่านี้เป็นค่าแรกและค่าที่สองตามลำดับและประกาศ `operator+` เพื่อความสะดวก
26+
27+
```cpp
28+
pair<long long, int> operator+(const pair<long long, int> &x,
29+
const pair<long long, int> &y) {
30+
return {x.first + y.first, x.second + y.second};
31+
}
32+
33+
pair<long long, int> S[MAX];
34+
int L[MAX]; // ดัชนีลูกซ้าย
35+
int R[MAX]; // ดัชนีลูกขวา
36+
```
37+
38+
ภาพตัวอย่าง Persistent Segment Tree ในสถานะหนึ่ง (แสดงเพียงค่าแรกในแต่ละ `pair` $S$ เพื่อความเข้าใจง่าย)
39+
40+
![](../media/o59_mar_c1_atleast/1.png)
41+
42+
### Update
43+
44+
การ Update จะคล้ายๆ Segment Tree ปกติ เพียงแต่แทนที่จะแก้ค่าที่เก็บไว้ที่แต่ละ Node โดยตรง หากมีการแก้ค่าจะต้องสร้าง Node ใหม่มาแทน Node เก่า
45+
46+
ในกรณีที่ช่อง $i$ ที่ถูก Update อยู่ในช่วง $[l,r]$ ที่รับผิดชอบของ Node จะต้องสร้างเป็น Node ใหม่ และหากรับผิดชอบมากกว่าหนึ่งช่องจะต้องแก้ลูกซ้ายขวาเป็น Node ที่ได้จากการ Update ลูกฝั่งซ้ายขวาเก่าเช่นกัน
47+
48+
กรณีที่ช่อง $i$ ที่ถูก Update ไม่อยู่ในช่วง $[l,r]$ เพียงต้อง return ตัว Node ปัจจุบันเพราะไม่มีอะไรต้องเปลี่ยน
49+
50+
ตัวอย่างโค้ดการ Update
51+
```cpp
52+
int last_segment_tree_node = 1;
53+
54+
int update(int i, int Z, int n, int l, int r) {
55+
if (l == i && i == r) { // The node only contains i
56+
int new_node = ++last_segment_tree_node;
57+
S[new_node] = {Z, 1};
58+
return new_node;
59+
}
60+
if (r < i || i < l) // i is not in the range
61+
return n;
62+
63+
int new_node = ++last_segment_tree_node;
64+
// i is in the range
65+
int mid = (l + r) / 2;
66+
L[new_node] = update(i, Z, L[n], l, mid);
67+
R[new_node] = update(i, Z, R[n], mid + 1, r);
68+
69+
S[new_node] = S[L[new_node]] + S[R[new_node]];
70+
return new_node;
71+
}
72+
```
73+
74+
เห็นได้ว่าการ Update นี้จะต่างกับของ Segment Tree ปกติตรงที่ return ดัชนีของ Node หลัง Update ไม่ใช่ค่าหลัง Update โดยแม้แต่ Node รากจะถูกแทนด้วย Node ที่สร้างใหม่
75+
76+
การ Update แต่ละครั้งจะเกิดการแก้ค่าได้อย่างมาก $\mathcal{O}(\log N)$ ครั้งตาม Segment Tree ปกติ แต่จะสร้าง Node เพิ่มขึ้นในขณะเดียวกันดังนั้นจึงมี Memory ที่ต้องใช้เพิ่มขึ้น $\mathcal{O}(\log N)$ เช่นกัน
77+
78+
ภาพตัวอย่างการ Update
79+
![](../media/o59_mar_c1_atleast/2.png)
80+
81+
Node สีเขียวแทน Node ที่สร้างขึ้นมาใหม่ใน Update นี้ ส่วนสีเหลืองแสดงว่าถูกเรียก Update แต่เพียง return Node เก่าเพราะไม่ต้องแก้อะไร
82+
83+
สังเกตว่า Node ทุกอันที่อยู่บนเส้นจากช่องล่างสุดที่ถูกแก้ค่าจะเป็น Node สร้างใหม่ (แต่ Node เก่าไม่ได้ถูกลบทิ้งและยังสามารถใช้ได้ใน Query ต่อๆ ไป)
84+
85+
### Query
86+
87+
สำหรับการ Query จะค่อนข้างคล้ายกับ Segment Tree ปกติ โดยต่างเพียงแค่ว่าจะต้อง Query ไปยังดัชนีของลูกแต่ละด้านที่อาจไม่ใช่ `n*2` กับ `n*2+1`
88+
89+
สำหรับการ Query ใน Persistent Segment Tree อาจมีหลาย Node ราก ซึ่งแต่ละรากจะแทนสถานะของ Segment หลังการ Update ครั้งหนึ่ง การจะ Query ที่สถานะที่ต้องการนั้นจึงเพียงต้องเลือกรากที่ถูกต้อง
90+
91+
```cpp
92+
pair<long long, int> query(int A, int B, int n, int l, int r) {
93+
if (A <= l && r <= B) // [l,r] is a subset of [a,b]
94+
return S[n];
95+
if (B < l || r < A) // [l,r] does not intersect [a,b]
96+
return {0, 0};
97+
98+
// [l,r] intersects [a,b]
99+
int mid = (l + r) / 2;
100+
auto left_query = query(A, B, L[n], l, mid);
101+
auto right_query = query(A, B, R[n], mid + 1, r);
102+
103+
return left_query + right_query;
104+
}
105+
106+
```
107+
การ Query ใช้เวลา $\mathcal{O}(\log N)$ ไม่ต่างกับ Segment Tree ปกติ
108+
109+
## Solution
110+
111+
ตามที่อธิบายไว้ในตอนแรก จะเริ่มจากการเรียง $idx[1], idx[2], \dots, idx[N]$ ให้ $X[idx[1]] \geq X[idx[2]] \geq \dots \geq X[idx[N]]$ และ Update แต่ละค่าที่ $idx[i]$ จาก $0$ เป็น $X[idx[i]]$ ทีละค่าใน Persistent Segment Tree โดยจะเก็บดัชนีของรากใหม่ที่ได้หลังแต่ละ Update
112+
113+
```cpp
114+
vector<int> idx(N + 1, 0);
115+
for (int i = 1; i <= N; i++)
116+
idx[i] = i;
117+
118+
sort(idx.begin() + 1, idx.begin() + N + 1,
119+
[&X](int i1, int i2) { return X[i1] > X[i2]; });
120+
121+
root_index[0] = 1;
122+
for (int i = 1; i <= N; i++)
123+
root_index[i] = update(idx[i], X[idx[i]], root_index[i - 1], 1, N);
124+
```
125+
126+
จากนั้นสำหรับแต่ละคำถามจะ Binary Search หาว่าสามารถเก็บได้มากสุดกี่ค่า โดยในแต่ละขั้นจะ Query ว่าถ้าพิจารณาทุกค่า $X[idx[1]], X[idx[1]], \dots, X[idx[mid]]$ จะได้ค่าเฉลี่ยเท่าไหร่และมีค่าที่เหลืออยู่เท่าไหร่ หากค่าเฉลี่ยนี้ไม่ต่ำกว่า $u$ แปลว่าสามารถเก็บอย่างน้อยจำนวนที่เหลือตอนนี้
127+
128+
```cpp
129+
cin >> s >> t >> u;
130+
131+
int best = 0;
132+
int b = 1;
133+
int e = N;
134+
135+
while (b <= e) {
136+
int mid = (b + e) / 2;
137+
auto query_result = query(s, t, root_index[mid], 1, N);
138+
long long sum = query_result.first;
139+
int c = query_result.second;
140+
if ((double)sum >= u * c - 1e-8) { // ใช้ 1e-8 เป็น Tolerance สำหรับ Floating-Point Comparison
141+
best = max(best, c);
142+
b = mid + 1;
143+
} else
144+
e = mid - 1;
145+
}
146+
147+
if (best == 0)
148+
cout << "-1\n";
149+
else
150+
cout << (t - s + 1) - best << "\n";
151+
```
152+
153+
#### Time Complexity
154+
การ Sort ค่า $idx$ ใช้เวลา $\mathcal{O}(N \log N)$
155+
156+
สำหรับ Persistent Segment Tree การ Update จะเกิดขึ้น $N$ ครั้ง ส่วนการ Query อาจะเกิดถึง $\mathcal{O}(M \log N)$ ครั้งเพราะแต่ละคำถามจะทำ Binary Search ซึ่งทำให้อาจต้อง Query ถึง $\mathcal{O}(\log N)$ ครั้ง
157+
158+
ดังนั้นทั้งหมดจะใช้เวลา $\mathcal{O}(N \log N + M \log^2 N)$
159+
160+

media/o59_mar_c1_atleast/1.png

58.4 KB
Loading

media/o59_mar_c1_atleast/2.png

64.8 KB
Loading

0 commit comments

Comments
 (0)