Skip to content

Commit

Permalink
Merge branch 'master' into add_1134
Browse files Browse the repository at this point in the history
  • Loading branch information
Thunyatorn authored Feb 13, 2024
2 parents 494dc33 + a669452 commit c8a6326
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 0 deletions.
39 changes: 39 additions & 0 deletions md/1038.md
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$
19 changes: 19 additions & 0 deletions md/1130.md
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 แล้วนับว่าแต่ละสมาชิกมีกี่สมาชิกที่มากกว่าหรือน้อยกว่าแทนการพิจารณาทุกคู่โดยตรง แต่นั่นไม่จำเป็นสำหรับข้อนี้)
110 changes: 110 additions & 0 deletions md/1142.md
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)$
Loading

0 comments on commit c8a6326

Please sign in to comment.