-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
544 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
ข้อนี้มีภารกิจ $N \leq 20$ ภารกิจ โดยต้องทำทุกภารกิจเพียงแต่ต้องเลือกลำดับที่จะทำ | ||
|
||
หากทำภารกิจที่ $j$ เป็นลำดับที่ $i$ จะมีโอกาสสำเร็จ $a_{(j,i)}$ โจทย์ถามว่าผลคูณความน่าจะเป็นเหล่านี้ที่เป็นไปได้มากสุดคือเท่าไหร่ | ||
|
||
### แนวคิด | ||
|
||
ข้อนี้เป็นโจทย์ Bitmask Dynamic Programming นั่นคือเป็นโจทย์ Dynamic Programming ที่เก็บ State เป็น Bitmask | ||
|
||
สังเกตว่าเราสามารถเก็บคำตอบของแต่ละ State เป็น $DP[S]$ ซึ่งแทนผลคูณความน่าจะเป็นที่จะสำเร็จโดยที่ภารกิจที่สำเร็จแล้วคือ $S$ เมื่อ State $S=(b_{N}b_{N-1}\dots b_0)_2$ เป็นเลขฐานสองโดยที่ $b_j=1$ ถ้าเราทำภารกิจที่ $j$ แล้ว คำตอบจะเป็น $DP[2^N -1]$ เพราะ $2^N-1 = (11\dots1)_2$ (มี 1 $N$ ตัว) | ||
|
||
เช่นถ้า $S=1010_2$ แสดงว่าทำภารกิจที่ 2 กับ 4 แล้ว | ||
|
||
สังเกตว่าสำหรับ State $S$ จำนวนภารกิจที่ทำไปแล้วจะเท่ากับจำนวน bit ที่เป็น $1$ ให้จำนวนนี้เป็น $i_{S}$ | ||
|
||
สำหรับ $DP[0]$ สามารถตั้งเป็น 100 แทนโอกาส 100% | ||
|
||
ในการคำนวณ $DP[S]$ สังเกตว่าจะต้องมีงานอันภารกิจ $j$ ที่ $b_j=1$ ใน $S$ ดังนั้นสามารถพิจารณาทีละงาน $j$ ดังกล่าวว่าผลคูณที่ดีที่สุดที่เป็นไปได้คือเท่าไหร่ ซึ่งจะได้ว่าเป็น $a_{(j, i_{S})} DP[S - (1<<j)]$ นั่นคือผลคูณของความน่าจะเป็นเมื่อทำงาน $j$ เป็นลำดับที่ $i$ กับผลคูณความน่าจะเป็นที่มากสุดที่เป็นไปได้สำหรับ $S - (1<<j)$ (ซึ่งเป็น State $S$ ก่อนทำภารกิจที่ $j$) | ||
|
||
ดังนั้นสำหรับแต่ละ $S$ หากมีค่า $DP[0], \dots, DP[S-1]$ แล้วจะใช้เวลาคำนวณเพียง $\mathcal{O}(N)$ เมื่อพิจารณาทีละภารกิจ | ||
|
||
ดังนั้นเมื่อต้องพิจารณา $2^N$ State เวลาทั้งหมดที่ใช้คือ $\mathcal{O}(N2^N)$ | ||
|
||
#### ตัวอย่างโค้ด | ||
|
||
```cpp | ||
dp[0] = 100.0; | ||
for (int s = 1; s <= ((1 << n) - 1); s++) { | ||
int i = 0; | ||
for (int j = 0; j < n; j++) | ||
i += (((1 << j) & s) != 0); | ||
|
||
dp[s] = 0; | ||
for (int j = 0; j < n; j++) | ||
if (((1 << j) & s) != 0) | ||
dp[s] = max(dp[s], dp[s ^ (1 << j)] * a[i - 1][j] / 100.0); | ||
} | ||
``` | ||
|
||
ตามคำอธิบายสำหรับแต่ละ $S$ จะนับจำนวนภารกิจที่สำเร็จแล้วใน State $S$ จากนั้นจะไล่ภารกิจที่สำเร็จใน $S$ ว่าควรทำอันไหนเป็นลำดับที่ $i$ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
ข้อนี้นิยามความจุของเซต $S$ เป็น | ||
- ค่าของสมาชิกตัวที่มากกว่าลบด้วยสมาชิกตัวที่น้อยกว่าสำหรับ $|S|=2$ | ||
- ผลรวมของความจุของสับเซตของ $S$ ทุกสับเซตที่มีสมาชิก $N − 1$ ตัว สำหรับ $|S|>2$ | ||
|
||
และให้หาความจุของเซ็ต $S$ ที่มีขนาด $|S|=N \leq 10000$ | ||
|
||
ข้อสังเกตหลักของข้อนี้คือเราสามารถคำนวณผลรวมของความจุของทุกสับเซ็ตที่มีขนาด $k$ ($k\geq 3$) ได้หากเราทราบผลรวมความจุของทุกสับเซตที่มีขนาด $k-1$ | ||
|
||
ให้ผลรวมความจุของสับเซ็ตขนาด $k$ ทุกอันเป็น $M_k$ | ||
|
||
สังเกตว่าทุกเซ็ตขนาด $k-1$ จะเป็นสับเซ็ตของสับเซ็ตขนาด $k$ จำนวน $N-(k-1)$ อันพอดี (เพราะเลือกอีก 1 สมาชิกของ $S$ ที่เหลืออยู่มาเพิ่ม จะมี $N-(k-1)$ ตัวเลือก) ดังนั้น $M_k = M_{k-1} (N-(k-1))$ | ||
|
||
สำหรับ $k=2$ เราสามารถคำนวณ $M_2$ ได้โดยตรงโดยการคำนวณค่ามากกว่าลบค่าน้อยกว่าสำหรับทุกคู่ของสมาชิกของ $S$ ในเวลา $\mathcal{O}(N^2)$ | ||
|
||
จากนั้นคำนวณ $M_3, M_4, \dots, M_N$ ได้ตามสูตร $M_k = M_{k-1} (N-(k-1))$ ซึ่งใช้เวลา $\mathcal{O}(N)$ | ||
|
||
ทั้งหมดจึงใช้เวลา $\mathcal{O}(N^2)$ ซึ่งเร็วเพียงพอสำหรับข้อนี้ | ||
|
||
(เพิ่มเติม: ขั้นตอนการหา $M_2$ สามารถลดเวลาเป็น $\mathcal{O}(N \log N)$ โดยการ sort แล้วนับว่าแต่ละสมาชิกมีกี่สมาชิกที่มากกว่าหรือน้อยกว่าแทนการพิจารณาทุกคู่โดยตรง แต่นั่นไม่จำเป็นสำหรับข้อนี้) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
ข้อนี้ให้บริษัทมา $N$ $(N\leq100000)$ บริษัทที่ต้องการจองศูนย์ประชุมซึ่งรองรับได้อย่างมากวันละ $K$ $(K\leq100)$ บริษัท | ||
|
||
บริษัท $i$ ต้องการจองศูนย์นี้ระหว่างวันที่ $X_i$ ถึง $Y_i$ $(1\leq X_i\leq Y_i\leq 1000000000)$ โดยการจองจะเรียงจาก $i=1$ ไปถึง $i=N$ หากวันใดมีบริษัทจองครบแล้ว $K$ บริษัท ศูนย์จะเต็มและไม่รับการจองของบริษัทต่อมาที่ขอจองในช่วงที่ทับวันดังกล่าว (กล่าวคือถ้าในช่วง $[X_i,Y_i]$ มีวันใดที่ครบ $K$ แล้วบริษัทที่ $i$ จะไม่ได้จองเลยทั้งช่วง $[X_i,Y_i]$) | ||
|
||
### แนวคิด | ||
|
||
อย่างแรกสังเกตว่าเราต้องสนใจเพียงวันที่ปรากฎใน $X_i$ หรือ $Y_i$ ใดๆ เป็นพิกัดเพราะหากมีการครบ $K$ วันจะต้องครบที่พิกัดดังกล่าว ดังนั้นสามารถทำ Coordinate Compression (เช่นเดียวกับใน https://programming.in.th/tasks/1138/solution) | ||
|
||
ในการพิจารณาแต่ละคำขอจองจากแต่ละบริษัทเราจะต้อง Query ว่าในช่วง $[X_i,Y_i]$ (ที่ทำ Coordinate Compression แล้ว) มีจองไปแล้วกี่บริษัทในวันที่ถูกจองมากที่สุด | ||
|
||
จากนั้นหากยังมีจองไม่ถึง $K$ จะต้อง Update ช่วงนี้ว่ามีจองเพิ่มขึ้น 1 บริษัทสำหรับทุกวันในช่วง $[X_i,Y_i]$ | ||
|
||
สังเกตว่า Operation ที่เราต้องการมีเพียง Query กับ Update คล้ายกับ Segment Tree เพียงแต่การ Update จะต้องรองรับการ Update ช่วงอย่างมีประสิทธิภาพด้วย โครงสร้างข้อมูลที่รองรับ Operation ดังกล่าวคือ Lazy Segment Tree | ||
|
||
### Lazy Segment Tree | ||
|
||
Lazy Segment Tree เป็นโครงสร้าง Segment Tree (อ่านเพิ่มได้จาก https://programming.in.th/tasks/1147/solution) ที่เพิ่มประสิทธิ์ภาพในการ Update แบบช่วง โดยในการ Update แบบช่วงจะเก็บเพิ่มอีกค่าในแต่ละ Node ซึ่งเป็นค่า "Lazy" ที่แทนว่าในทั้งช่วงนี้ยังค้าง Update อะไรอยู่ | ||
|
||
รูปต่อไปนี้เป็น Lazy Segment Tree ที่เริ่มด้วยค่า Lazy เป็น 0 ทั้งหมด | ||
|
||
![](../media/1142/0.png) | ||
|
||
#### Update | ||
|
||
สำหรับการ `update` เพิ่มช่วง $[X_i,Y_i]$ ด้วยค่า $Z$ จะคล้ายๆ กับ Segment Tree ปกติ โดยต่างกันเพียงแค่ | ||
- ถ้าช่วง $[l,r]$ ที่ Node นี้รับผิดชอบอยู่ใน $[X_i, Y_i]$ จะสามารถเพิ่มค่า Lazy ของ Node นี้ได้แล้ว return เลยโดยไม่ต้องลงไปใน Node ซ้ายหรือยวา | ||
- ถ้า $[l,r]$ ตัดกับ $[X_i, Y_i]$ แต่ไม่ได้อยู่ข้างในทั้งหมดจะต้อง Push ค่า Lazy ไปยัง Node ซ้ายและขวาโดยใช้การ `update` ด้วยค่า Lazy ดังกล่าว | ||
|
||
ตัวอย่างเช่น | ||
|
||
![](../media/1142/1.png) | ||
|
||
Node สีฟ้าคือ Node ที่ช่วงทั้งช่วง $[l,r]$ อยู่ใน Update ซึ่งจะโดนแก้เพียงค่า Lazy เช่นสำหรับ Node ที่คุมช่วง $C[1..3]$ สังเกตว่าสำหรับ Node เหล่านี้ Node ด้านล่างไม่ถูก Visit เลย | ||
|
||
ตัวอย่างโค้ด | ||
|
||
```cpp | ||
int update(int X, int Y, int Z, int n, int l, int r) { | ||
if (X <= l && r <= Y) { // [l,r] is contained in [X,Y] | ||
Lazy[n] += Z; | ||
return ST[n] + Lazy[n]; | ||
} | ||
if (r < X || Y < l) // [X,Y] is not in the range | ||
return ST[n] + Lazy[n]; | ||
|
||
// [l,r] intersects [X,Y] | ||
push_lazy(n, l, r); | ||
|
||
int mid = (l + r) / 2; | ||
int new_left_value = update(X, Y, Z, n * 2, l, mid); | ||
int new_right_value = update(X, Y, Z, n * 2 + 1, mid + 1, r); | ||
|
||
ST[n] = max(new_left_value, new_right_value); | ||
return ST[n]; | ||
} | ||
``` | ||
#### Push Lazy | ||
สำหรับการ Push ค่า Lazy เราเพียงต้อง `update` Node ลูกซ้ายและขวาด้วยค่า Lazy ปัจจุบัน และแก้ค่า Lazy ให้เป็น $0$ เพื่อแสดงว่าไม่เหลือ Lazy Update ที่ค้างอยู่แล้ว เสร็จแล้วต้องแก้ค่า $\max$ ของช่วงที่เก็บไว้เพื่อให้ถูกต้องหลัง `update` ลูก | ||
ตัวอย่างโค้ด | ||
```cpp | ||
void push_lazy(int n, int l, int r) { | ||
if (Lazy[n] == 0) | ||
return; | ||
int mid = (l + r) / 2; | ||
int new_left_value = update(l, r, Lazy[n], n * 2, l, mid); | ||
int new_right_value = update(l, r, Lazy[n], n * 2 + 1, mid + 1, r); | ||
Lazy[n] = 0; | ||
ST[n] = max(new_left_value, new_right_value); | ||
} | ||
``` | ||
|
||
สังเกตว่าในการ `update` แต่ละครั้งทุกขั้นตอนรวมถึงการ `push_lazy` จะใช้เวลา $\mathcal{O}(1)$ ดังนั้น Time Complexity จะเป็นไปตามจำนวน Node ที่ถูก Visit ซึ่งสามารถพิสูจน์ได้จำนวน Node ที่ต้อง Visit จะเป็น $\mathcal{O}(\log N)$ เช่นเดียวกับที่พิสูจน์ไว้แล้วสำหรับ `update` ของ Segment Tree ทั่วไปใน https://programming.in.th/tasks/1147/solution | ||
|
||
#### Query | ||
|
||
สำหรับการ `query` จะคล้าย `query` ของ Segment Tree ปกติ โดยเพียงต้องบวกค่า Lazy เข้าไปในค่าที่ Return หาก $[l,r]$ อยู่ในช่วง Query และต้อง `push_lazy` เช่นเดียวกับการ `update` หาก $[l,r]$ ตัดกันช่วง Query แต่ไม่ได้อยู่ข้างในช่วงทั้งหมด | ||
|
||
ตัวอย่างประกอบ | ||
|
||
![](../media/1142/2.png) | ||
|
||
ในตัวอย่างนี้ค่าที่ถูก Return คือค่าที่มากสุดของ Node ที่อยู่ในช่วง (Node สีฟ้า) สังเกตว่า Node สำหรับ $C[4]$ กับ $C[5]$ ถูก `push_lazy` จากค่า Lazy ของ Node $C[4..5]$ | ||
|
||
ตัวอย่างโค้ด | ||
```cpp | ||
int query(int A, int B, int n, int l, int r) { | ||
if (A <= l && r <= B) // [l,r] is a subset of [a,b] | ||
return ST[n] + Lazy[n]; | ||
if (B < l || r < A) // [l,r] does not intersect [a,b] | ||
return -1000000001; // -inf | ||
|
||
// [l,r] intersects [a,b] | ||
push_lazy(n, l, r); | ||
|
||
int mid = (l + r) / 2; | ||
int left_query = query(A, B, n * 2, l, mid); | ||
int right_query = query(A, B, n * 2 + 1, mid + 1, r); | ||
|
||
return max(left_query, right_query); | ||
} | ||
``` | ||
การ `query` จะใช้เวลา $\mathcal{O}(\log N)$ เช่นเดียวกับการ `update` | ||
### Time Complexity | ||
ตามที่อธิบายไว้ด้านบนเราจะทำ Coordinate Compression ที่ใช้เวลา $\mathcal{O}(N\log N)$ แล้วใช้ Lazy Segment Tree สำหรับการ `update` และ `query` โดยแต่ละ Operation ใช้เวลา $\mathcal{O}(\log N)$ (เพราะจำนวนพิกัดที่ใช้ใน Lazy Segment Tree หลัง Compression คืออย่างมาก $2N$) ซึ่งต้องทำ $N$ ครั้ง จึงใช้เวลาทั้งหมด $\mathcal{O}(N\log N)$ |
Oops, something went wrong.