[0]zero-index clytie0x/zero-index

21.6 แบบฝึกหัด

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

ข้อ 1 ขนมสามชิ้น

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

ร้านสหกรณ์โรงเรียนมีขนม N ชิ้น ชิ้นที่ i ราคา pi บาท ทุกชิ้นต่างกันแม้ราคาจะเท่ากัน น้องมีเงิน M บาท อยากซื้อขนมให้เพื่อนสามคน คนละหนึ่งชิ้น จึงต้องเลือกขนมสามชิ้นที่ต่างกัน ราคารวมต้องไม่เกิน M และอยากใช้เงินให้คุ้มที่สุด คือราคารวมมากที่สุดเท่าที่ทำได้

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม N และ M คั่นด้วยช่องว่าง โดยที่ 3 ≤ N ≤ 100 และ 1 ≤ M ≤ 3000000

บรรทัดที่สองมีจำนวนเต็ม p1 ถึง pN คั่นด้วยช่องว่าง โดยที่ 1 ≤ pi ≤ 1000000

ข้อมูลออก

ราคารวมที่มากที่สุดของขนมสามชิ้นที่ต่างกันซึ่งไม่เกิน M ถ้าเลือกสามชิ้นแบบไหนก็เกิน M ให้พิมพ์ -1

5 100
35 20 48 27 60
95
4 10
5 4 6 8
-1
ดูคำใบ้

N ไม่เกิน 100 จำนวนแบบของการเลือกสามชิ้นคือ C(N, 3) ไม่ถึงสองแสน ลองทุกแบบได้สบายด้วยลูปซ้อนสามชั้นแบบหน้า 21.1 ให้ j เริ่มที่ i + 1 และ k เริ่มที่ j + 1 จะได้ชิ้นที่ต่างกันเสมอ ถ้าเริ่มทุกลูปที่ 0 จะหยิบชิ้นเดิมซ้ำได้ เก็บคำตอบเริ่มต้นเป็น -1 แล้วอัปเดตเมื่อเจอผลรวมที่ไม่เกินงบและมากกว่าเดิม

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n, budget;
    cin >> n >> budget;
    vector<int> price(n);
    for (int i = 0; i < n; i++) {
        cin >> price[i];
    }
    int best = -1;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            for (int k = j + 1; k < n; k++) {
                int total = price[i] + price[j] + price[k];
                if (total <= budget && total > best) {
                    best = total;
                }
            }
        }
    }
    cout << best << '\n';
    return 0;
}

ข้อ 2 สวิตช์ไฟหอประชุม

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

หอประชุมของโรงเรียนมีหลอดไฟ M ดวง หมายเลข 1 ถึง M และมีสวิตช์ N ตัว ช่างไฟต่อสายไว้แปลกๆ สวิตช์แต่ละตัวต่อกับหลอดหลายดวง กดหนึ่งครั้งแล้วทุกหลอดที่ต่ออยู่จะสลับสถานะ ดวงที่ดับจะติด ดวงที่ติดจะดับ ตอนนี้ไฟดับทุกดวง ภารโรงอยากเปิดไฟให้ติดครบทุกดวงโดยกดสวิตช์ให้น้อยครั้งที่สุด

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม N และ M คั่นด้วยช่องว่าง โดยที่ 1 ≤ N ≤ 20 และ 1 ≤ M ≤ 20

อีก N บรรทัด บรรทัดที่ i บอกสวิตช์ตัวที่ i ขึ้นต้นด้วยจำนวนเต็ม k (1 ≤ k ≤ M) ตามด้วยหมายเลขหลอด k ตัวที่สวิตช์นี้ต่ออยู่ ไม่ซ้ำกัน แต่ละตัวอยู่ระหว่าง 1 ถึง M

ข้อมูลออก

จำนวนครั้งที่กดน้อยที่สุดที่ทำให้ไฟติดครบทุกดวง ถ้าทำไม่ได้ให้พิมพ์ -1

3 4
2 1 2
2 2 3
2 3 4
2
2 3
2 1 2
2 2 3
-1
ดูคำใบ้

กดสวิตช์เดิมสองครั้งเท่ากับไม่ได้กด และลำดับการกดไม่มีผล แต่ละสวิตช์จึงมีแค่สองทาง กดหรือไม่กด N ไม่เกิน 20 ลองทุกเซตย่อยด้วย bitmask แบบหน้า 21.2 ได้ เก็บสวิตช์แต่ละตัวเป็นตัวเลขที่ bit ที่ j - 1 เป็น 1 ถ้าสวิตช์นั้นสลับหลอดที่ j สถานะหลอดหลังกดคือ XOR (^ ในหน้า 12.3) ของทุกสวิตช์ที่เลือก ไม่ใช่ OR เพราะหลอดที่โดนสลับสองครั้งจะดับ เทียบกับ (1 << M) - 1 แล้วนับจำนวนสวิตช์ด้วย __builtin_popcount

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n, m;
    cin >> n >> m;
    vector<int> effect(n, 0);
    for (int i = 0; i < n; i++) {
        int k;
        cin >> k;
        for (int t = 0; t < k; t++) {
            int lamp;
            cin >> lamp;
            effect[i] |= 1 << (lamp - 1);
        }
    }
    int all_on = (1 << m) - 1;
    int best = -1;
    for (int mask = 0; mask < (1 << n); mask++) {
        int lamps = 0;
        for (int i = 0; i < n; i++) {
            if ((mask >> i) & 1) {
                lamps ^= effect[i];
            }
        }
        if (lamps == all_on) {
            int presses = __builtin_popcount(mask);
            if (best == -1 || presses < best) {
                best = presses;
            }
        }
    }
    cout << best << '\n';
    return 0;
}

ข้อ 3 ส่งพัสดุให้ครบทุกบ้าน

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

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

ระยะทางจากจุด i ไปจุด j คือ d[i][j] กิโลเมตร ถนนบางเส้นเป็นทางเดินรถทางเดียว d[i][j] จึงอาจไม่เท่ากับ d[j][i] ให้หาระยะทางรวมที่สั้นที่สุด

ข้อมูลเข้า

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

อีก N + 1 บรรทัด บรรทัดที่ i (นับจาก 0) มีจำนวนเต็ม N + 1 ตัว คือ d[i][0] ถึง d[i][N] คั่นด้วยช่องว่าง โดยที่ d[i][i] = 0 และ 1 ≤ d[i][j] ≤ 1000 เมื่อ i ≠ j

ข้อมูลออก

ระยะทางรวมที่สั้นที่สุด

3
0 4 9 7
4 0 3 8
9 3 0 2
7 8 2 0
9
2
0 5 1
5 0 10
1 1 0
2
ดูคำใบ้

ลำดับการไปบ้านคือการเรียงสับเปลี่ยนของ 1 ถึง N มี N! แบบ 9! = 362880 แบบ แต่ละแบบบวกระยะ N ครั้ง ไม่ถึงสี่ล้านครั้ง ลองทุกลำดับด้วย next_permutation แบบหน้า 21.2 ได้ เริ่มจาก vector ที่เรียงแล้วคือ 1 2 ... N อย่าใส่ร้าน (จุด 0) ลงไปในลำดับ เพราะร้านต้องเป็นจุดเริ่มเสมอ ระยะของลำดับหนึ่งคือ d[0][ลำดับแรก] บวกระยะระหว่างบ้านที่ติดกันในลำดับ ระวังว่า d[i][j] กับ d[j][i] อาจไม่เท่ากันเพราะถนนเดินรถทางเดียว

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    cin >> n;
    vector<vector<int>> dist(n + 1, vector<int>(n + 1));
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            cin >> dist[i][j];
        }
    }
    vector<int> order(n);
    for (int i = 0; i < n; i++) {
        order[i] = i + 1;
    }
    int best = INT_MAX;
    do {
        int total = dist[0][order[0]];
        for (int i = 0; i + 1 < n; i++) {
            total += dist[order[i]][order[i + 1]];
        }
        best = min(best, total);
    } while (next_permutation(order.begin(), order.end()));
    cout << best << '\n';
    return 0;
}

ข้อ 4 ขนาดกล่องพัสดุ

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

บริษัทขนส่งจะผลิตกล่องพัสดุทรงสี่เหลี่ยมมุมฉากรุ่นใหม่ ที่จุของได้ V ลูกบาศก์เซนติเมตรพอดี ด้านกว้าง ยาว และสูงต้องเป็นจำนวนเต็มเซนติเมตร กล่องที่หมุนแล้วได้รูปเดิมนับเป็นขนาดเดียวกัน เช่น 1 × 2 × 3 กับ 3 × 1 × 2 จึงเขียนทุกขนาดเป็น a × b × c ที่ a ≤ b ≤ c ได้แบบเดียว

ฝ่ายผลิตอยากรู้ว่ามีกล่องที่จุ V ได้พอดีกี่ขนาด และขนาดที่ใช้กระดาษน้อยที่สุด ซึ่งก็คือขนาดที่พื้นที่ผิว 2(ab + bc + ca) น้อยที่สุด ใช้กระดาษกี่ตารางเซนติเมตร

ข้อมูลเข้า

บรรทัดเดียว มีจำนวนเต็ม V โดยที่ 1 ≤ V ≤ 1010

ข้อมูลออก

บรรทัดเดียว มีจำนวนเต็มสองตัวคั่นด้วยช่องว่าง คือจำนวนขนาดของกล่องที่จุ V ได้พอดี และพื้นที่ผิวที่น้อยที่สุดเป็นตารางเซนติเมตร

12
4 32
7
1 30
ดูคำใบ้

นับก่อนลอง วนกว้าง ยาว สูงครบสามชั้นถึง V ไม่มีทางทัน แต่พอเลือก a กับ b แล้ว c ถูกบังคับให้เป็น V / (a × b) ไม่ต้องวนหา แค่ตรวจว่าหารลงตัว แบบตัดลูปชั้นในในหน้า 21.1 ถึงอย่างนั้น ถ้าวน a ไปถึง V และวน b ไปจน a × b เกิน V ก็ยังต้องทำราว 1011 รอบเมื่อ V = 1010 ไม่ทัน ให้ใช้เงื่อนไข a ≤ b ≤ c จำกัดลูป a ≤ b ≤ c แปลว่า a × a × a ≤ V จึงวน a แค่ถึง 2154 และ a × b × b ≤ V จึงวน b แค่ถึงราว √(V / a) เหลือไม่ถึง 107 รอบ และได้ c ≥ b มาเองโดยไม่ต้องตรวจ V ใหญ่เกิน int และพื้นที่ผิวของกล่อง 1 × 1 × V ได้ถึงราว 4 × 1010 ใช้ long long ทุกตัว

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int main()
{
    long long volume;
    cin >> volume;
    long long sizes = 0;
    long long best = LLONG_MAX;
    for (long long a = 1; a * a * a <= volume; a++) {
        for (long long b = a; a * b * b <= volume; b++) {
            if (volume % (a * b) == 0) {
                long long c = volume / (a * b);
                sizes++;
                best = min(best, 2 * (a * b + b * c + c * a));
            }
        }
    }
    cout << sizes << ' ' << best << '\n';
    return 0;
}

ข้อ 5 กระดานหมากรุกชำรุด

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

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

ข้อมูลเข้า

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

อีก N บรรทัด แต่ละบรรทัดเป็นข้อความยาว N ตัว ตัว . คือช่องที่ยังดี และ # คือช่องที่แตก

ข้อมูลออก

จำนวนแบบของการวางควีน N ตัว

4
....
....
....
....
2
5
.....
.#...
.....
...#.
.....
6
ดูคำใบ้

วางทีละแถวแบบ N-Queens ในหน้า 21.3 ใช้อาเรย์จำคอลัมน์และแนวทแยงทั้งสองทิศที่ถูกใช้แล้ว เพิ่มเงื่อนไขเดียวคือช่อง # วางไม่ได้ ถ้าลองทุกการเรียงสับเปลี่ยนของคอลัมน์ด้วย next_permutation แล้วค่อยตรวจทีหลัง เมื่อ N = 12 ต้องตรวจ 12! ราว 4.8 × 108 แบบ ไม่ทันเวลา ต้องตัดตั้งแต่แถวแรกที่ขัดกฎ คำตอบที่ N = 12 ไม่เกิน 14200 จึงใช้ int ได้

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int n;
vector<string> board;
bool col_used[12], diag1_used[23], diag2_used[23];
int ways = 0;

void place(int row)
{
    if (row == n) {
        ways++;
        return;
    }
    for (int c = 0; c < n; c++) {
        int d1 = row + c, d2 = row - c + n - 1;
        if (board[row][c] == '#' || col_used[c] || diag1_used[d1] || diag2_used[d2]) {
            continue;
        }
        col_used[c] = diag1_used[d1] = diag2_used[d2] = true;
        place(row + 1);
        col_used[c] = diag1_used[d1] = diag2_used[d2] = false;
    }
}

int main()
{
    cin >> n;
    board.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> board[i];
    }
    place(0);
    cout << ways << '\n';
    return 0;
}

ข้อ 6 ซูโดกุหกช่อง

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

หนังสือพิมพ์ของโรงเรียนมีมุมปริศนาเป็นซูโดกุขนาด 6 × 6 ตารางแบ่งเป็นกล่องเล็ก 6 กล่อง แต่ละกล่องสูง 2 แถว กว้าง 3 คอลัมน์ ต้องเติมเลข 1 ถึง 6 ลงทุกช่อง ให้ทุกแถว ทุกคอลัมน์ และทุกกล่องมีเลข 1 ถึง 6 ครบ ตัวละหนึ่งครั้ง บางช่องมีเลขให้มาแล้ว

ปริศนาบางข้อมีหลายคำตอบ ให้พิมพ์คำตอบที่มาก่อนเมื่ออ่านเลขเรียงทีละแถวจากซ้ายไปขวา บนลงล่าง เทียบกันที่ช่องแรกที่ต่างกัน คำตอบที่เลขในช่องนั้นน้อยกว่ามาก่อน ถ้าเติมไม่ได้ หรือเลขที่ให้มาขัดกฎกันอยู่แล้ว ให้พิมพ์ NO

ข้อมูลเข้า

6 บรรทัด บรรทัดละ 6 จำนวน คั่นด้วยช่องว่าง แต่ละจำนวนอยู่ระหว่าง 0 ถึง 6 เลข 0 คือช่องว่าง

ข้อมูลออก

ตารางที่เติมเสร็จ 6 บรรทัด บรรทัดละ 6 จำนวน คั่นด้วยช่องว่างหนึ่งช่อง หรือ NO บรรทัดเดียว

0 0 3 0 1 0
5 6 0 3 2 0
0 5 4 2 0 3
2 0 6 4 5 0
0 1 2 0 4 5
0 4 0 1 0 0
4 2 3 5 1 6
5 6 1 3 2 4
1 5 4 2 6 3
2 3 6 4 5 1
3 1 2 6 4 5
6 4 5 1 3 2
0 2 3 4 5 6
0 0 0 0 0 0
1 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
NO
ดูคำใบ้

ใช้โครงซูโดกุ 4 × 4 ในหน้า 21.3 เติมช่องว่างทีละช่องจากซ้ายไปขวา บนลงล่าง ลองเลข 1 ถึง 6 จากน้อยไปมาก แล้วหยุดทันทีที่เติมครบ คำตอบแรกที่เจอจะเป็นแบบที่โจทย์ต้องการเอง กล่องสูง 2 แถว กว้าง 3 คอลัมน์ มุมซ้ายบนของกล่องคือแถว r / 2 * 2 คอลัมน์ c / 3 * 3 ระวังเลขที่ให้มาตั้งแต่ต้นอาจขัดกฎกันเอง ตัวค้นหาตรวจเฉพาะเลขที่เราเติม จึงต้องตรวจเลขที่ให้มาก่อนเริ่มค้น ทั้งแถว คอลัมน์ และกล่อง

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int grid[6][6];

bool can_put(int r, int c, int d)
{
    for (int k = 0; k < 6; k++) {
        if ((k != c && grid[r][k] == d) || (k != r && grid[k][c] == d)) {
            return false;
        }
    }
    int top = r / 2 * 2, left = c / 3 * 3;
    for (int i = top; i < top + 2; i++) {
        for (int j = left; j < left + 3; j++) {
            if ((i != r || j != c) && grid[i][j] == d) {
                return false;
            }
        }
    }
    return true;
}

bool solve(int pos)
{
    if (pos == 36) {
        return true;
    }
    int r = pos / 6, c = pos % 6;
    if (grid[r][c] != 0) {
        return solve(pos + 1);
    }
    for (int d = 1; d <= 6; d++) {
        if (can_put(r, c, d)) {
            grid[r][c] = d;
            if (solve(pos + 1)) {
                return true;
            }
            grid[r][c] = 0;
        }
    }
    return false;
}

int main()
{
    for (int r = 0; r < 6; r++) {
        for (int c = 0; c < 6; c++) {
            cin >> grid[r][c];
        }
    }
    bool ok = true;
    for (int r = 0; r < 6; r++) {
        for (int c = 0; c < 6; c++) {
            if (grid[r][c] != 0 && !can_put(r, c, grid[r][c])) {
                ok = false;
            }
        }
    }
    if (!ok || !solve(0)) {
        cout << "NO\n";
        return 0;
    }
    for (int r = 0; r < 6; r++) {
        for (int c = 0; c < 6; c++) {
            cout << grid[r][c] << (c < 5 ? ' ' : '\n');
        }
    }
    return 0;
}

ข้อ 7 เทน้ำสามขวด

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

ห้องแล็บเคมีมีขวดสามใบไม่มีขีดบอกปริมาตร ขวด A จุได้ A มิลลิลิตร ขวด B จุ B มิลลิลิตร และขวด C จุ C มิลลิลิตร ตอนเริ่มขวดทั้งสามมีน้ำ a b และ c มิลลิลิตรตามลำดับ ไม่มีก๊อกน้ำ และห้ามเทน้ำทิ้ง สิ่งที่ทำได้มีอย่างเดียว คือเทน้ำจากขวดหนึ่งไปอีกขวดหนึ่ง จนกว่าขวดต้นทางจะหมด หรือขวดปลายทางจะเต็ม แล้วแต่อย่างไหนเกิดก่อน

สถานะคือปริมาณน้ำในสามขวด ให้หาว่าเมื่อเทกี่ครั้งก็ได้ จะไปถึงสถานะที่ต่างกันได้กี่แบบ นับสถานะเริ่มต้นด้วย และขวด A มีน้ำได้กี่มิลลิลิตรบ้าง

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม A B C คั่นด้วยช่องว่าง โดยที่ 1 ≤ A, B, C ≤ 100

บรรทัดที่สองมีจำนวนเต็ม a b c คั่นด้วยช่องว่าง โดยที่ 0 ≤ a ≤ A, 0 ≤ b ≤ B และ 0 ≤ c ≤ C

ข้อมูลออก

บรรทัดแรกพิมพ์จำนวนสถานะที่ไปถึงได้ บรรทัดที่สองพิมพ์ปริมาณน้ำที่เป็นไปได้ทั้งหมดของขวด A เรียงจากน้อยไปมาก ไม่ซ้ำ คั่นด้วยช่องว่างหนึ่งช่อง

3 5 8
0 0 8
16
0 1 2 3
2 4 6
0 1 4
11
0 1 2
ดูคำใบ้

สถานะคือปริมาณน้ำในสามขวด น้ำไม่หายไปไหน ถ้ารู้ปริมาณในขวด A กับ B ก็รู้ขวด C เอง จึงใช้ visited[a][b] ขนาด 101 × 101 ตามแถวเทน้ำในตารางแทนสถานะแบบอื่นของหน้า 21.5 ส่วนการค้นเขียนแบบ DFS ในตัวอย่างข้ามแม่น้ำ ซึ่งใช้ visited[16] มิติเดียวเพราะสถานะเป็น bitmask จากสถานะหนึ่งลองเททั้ง 6 แบบ คือจากขวด x ไปขวด y ที่ x ≠ y ปริมาณที่เทคือค่าน้อยกว่าระหว่างน้ำในขวด x กับที่ว่างในขวด y แล้ว DFS ไปทุกสถานะใหม่ที่ยังไม่เคยไป นับจำนวนสถานะที่ไปถึง และจดว่าขวด A เคยมีน้ำเท่าไรบ้าง ลืมเช็ก visited แล้วจะเทกลับไปกลับมาไม่รู้จบจน stack เต็ม

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int cap[3];
int total;
bool visited[101][101];
bool a_possible[101];
int states = 0;

void dfs(int a, int b)
{
    visited[a][b] = true;
    states++;
    a_possible[a] = true;
    int water[3] = {a, b, total - a - b};
    for (int from = 0; from < 3; from++) {
        for (int to = 0; to < 3; to++) {
            if (from == to) {
                continue;
            }
            int amount = min(water[from], cap[to] - water[to]);
            int next[3] = {water[0], water[1], water[2]};
            next[from] -= amount;
            next[to] += amount;
            if (!visited[next[0]][next[1]]) {
                dfs(next[0], next[1]);
            }
        }
    }
}

int main()
{
    int a, b, c;
    cin >> cap[0] >> cap[1] >> cap[2];
    cin >> a >> b >> c;
    total = a + b + c;
    dfs(a, b);
    cout << states << '\n';
    bool first = true;
    for (int x = 0; x <= cap[0]; x++) {
        if (a_possible[x]) {
            cout << (first ? "" : " ") << x;
            first = false;
        }
    }
    cout << '\n';
    return 0;
}

ข้อ 8 แบ่งขนมใส่ถุงให้หนักเท่ากัน

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

ร้านขนมได้รับขนมมา N ห่อ ห่อที่ i หนัก wi กรัม เจ้าของร้านอยากจัดกระเช้าของขวัญ K ถุง โดยต้องใช้ขนมครบทุกห่อ ห้ามแกะหรือแบ่งห่อ แต่ละห่ออยู่ในถุงเดียว และทุกถุงต้องหนักเท่ากันพอดี ให้ตอบว่าทำได้หรือไม่

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม N และ K คั่นด้วยช่องว่าง โดยที่ 1 ≤ N ≤ 20 และ 2 ≤ K ≤ 5

บรรทัดที่สองมีจำนวนเต็ม w1 ถึง wN คั่นด้วยช่องว่าง โดยที่ 1 ≤ wi ≤ 1000000

ข้อมูลออก

YES ถ้าแบ่งได้ หรือ NO ถ้าแบ่งไม่ได้

6 3
5 4 3 3 2 1
YES
4 2
3 3 3 5
NO
ดูคำใบ้

ทุกถุงต้องหนักเท่ากับผลรวมหาร K ถ้าหารไม่ลงตัว หรือห่อที่หนักที่สุดหนักเกินนั้น ตอบ NO ได้เลย ลองใส่ทุกห่อลงทุกถุงมี KN แบบ 520 เกินไปมาก แม้จะตัดเมื่อถุงหนักเกินก็ยังช้าเมื่อคำตอบเป็น NO ต้องตัดกิ่งเพิ่ม วิธีหนึ่งคือเติมทีละถุงให้ได้น้ำหนักพอดี เรียงห่อจากหนักไปเบา ถุงใหม่ทุกถุงเริ่มด้วยห่อที่หนักที่สุดที่ยังเหลือ เพราะห่อนั้นต้องอยู่ในถุงใดถุงหนึ่งอยู่แล้ว ถ้าเริ่มแบบนั้นแล้วไม่สำเร็จ ก็ไม่ต้องลองแบบอื่น ภายในถุงเดียวกันเลือกห่อตามลำดับจากหน้าไปหลังเหมือนเลือกเซตย่อย และถ้าห่อน้ำหนักเท่านี้เพิ่งลองแล้วไม่สำเร็จ ห่อถัดไปที่หนักเท่ากันก็ข้ามได้ เติมครบ K - 1 ถุงแล้ว ห่อที่เหลือหนักพอดีถุงสุดท้ายเอง

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int n, k;
long long target;
vector<long long> weight;
vector<bool> used;

bool fill_bag(int bags_done, int start, long long load)
{
    if (bags_done == k - 1) {
        return true;
    }
    if (load == target) {
        return fill_bag(bags_done + 1, 0, 0);
    }
    long long last_failed = -1;
    for (int i = start; i < n; i++) {
        if (used[i] || weight[i] == last_failed || load + weight[i] > target) {
            continue;
        }
        used[i] = true;
        if (fill_bag(bags_done, i + 1, load + weight[i])) {
            return true;
        }
        used[i] = false;
        last_failed = weight[i];
        if (load == 0) {
            break;
        }
    }
    return false;
}

int main()
{
    cin >> n >> k;
    weight.resize(n);
    long long total = 0;
    for (int i = 0; i < n; i++) {
        cin >> weight[i];
        total += weight[i];
    }
    sort(weight.rbegin(), weight.rend());
    used.assign(n, false);
    target = total / k;
    bool ok = total % k == 0 && weight[0] <= target && fill_bag(0, 0, 0);
    cout << (ok ? "YES" : "NO") << '\n';
    return 0;
}

ข้อ 9 จัดเวรครู

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

สัปดาห์สอบมีเวรคุมสอบ N เวร และมีครู N คน ครูหนึ่งคนรับได้หนึ่งเวร และทุกเวรต้องมีครูรับหนึ่งคน ครูแต่ละคนเหนื่อยกับแต่ละเวรไม่เท่ากัน เพราะบ้านไกลใกล้และวิชาที่สอนต่างกัน ถ้าครูคนที่ i รับเวรที่ j จะเหนื่อย cij หน่วย ฝ่ายวิชาการอยากจัดเวรให้ความเหนื่อยรวมของครูทุกคนน้อยที่สุด

ข้อมูลเข้า

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

อีก N บรรทัด บรรทัดที่ i มีจำนวนเต็ม ci1 ถึง ciN คั่นด้วยช่องว่าง โดยที่ 1 ≤ cij ≤ 1000000

ข้อมูลออก

ความเหนื่อยรวมที่น้อยที่สุด

3
4 2 8
4 3 7
3 1 6
12
2
9 1
1 9
2
ดูคำใบ้

ลองทุกการจับคู่คือ N! แบบ 13! เกินหกพันล้าน ไม่ทัน ใช้ branch and bound แบบหน้า 21.4 จัดเวรให้ครูทีละคน จำว่าเวรไหนมีคนรับแล้ว และเก็บผลรวมที่ดีที่สุดที่เจอ ก่อนลงไปต่อ ให้ประมาณแบบมองโลกในแง่ดีว่าครูที่เหลือแต่ละคนจะได้เวรที่ตัวเองเหนื่อยน้อยที่สุดในบรรดาเวรที่ยังว่าง (ไม่สนว่าจะชนกัน) ถ้าผลรวมตอนนี้บวกค่าประมาณนี้ไม่น้อยกว่าคำตอบที่ดีที่สุด ตัดกิ่งได้ อีกเรื่องที่ช่วยมากคือให้ครูแต่ละคนลองเวรที่เหนื่อยน้อยไปมาก จะได้คำตอบดีๆ เร็ว แล้วกิ่งที่เหลือถูกตัดง่ายขึ้น เรียงลำดับเวรของครูแต่ละคนไว้ก่อนครั้งเดียว

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

int n;
vector<vector<int>> cost;
vector<vector<int>> cheapest_first;
vector<bool> taken;
long long best = LLONG_MAX;

long long optimistic_rest(int teacher)
{
    long long sum = 0;
    for (int t = teacher; t < n; t++) {
        for (int duty : cheapest_first[t]) {
            if (!taken[duty]) {
                sum += cost[t][duty];
                break;
            }
        }
    }
    return sum;
}

void go(int teacher, long long total)
{
    if (teacher == n) {
        best = min(best, total);
        return;
    }
    if (total + optimistic_rest(teacher) >= best) {
        return;
    }
    for (int duty : cheapest_first[teacher]) {
        if (!taken[duty]) {
            taken[duty] = true;
            go(teacher + 1, total + cost[teacher][duty]);
            taken[duty] = false;
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    cost.assign(n, vector<int>(n));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            cin >> cost[i][j];
        }
    }
    cheapest_first.assign(n, vector<int>(n));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            cheapest_first[i][j] = j;
        }
        sort(cheapest_first[i].begin(), cheapest_first[i].end(), [&](int a, int b) {
            return cost[i][a] < cost[i][b];
        });
    }
    taken.assign(n, false);
    go(0, 0);
    cout << best << '\n';
    return 0;
}

ข้อ 10 นับตะกร้าที่ไม่เกินงบ

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

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

ข้อมูลเข้า

บรรทัดแรกมีจำนวนเต็ม N และ S คั่นด้วยช่องว่าง โดยที่ 1 ≤ N ≤ 40 และ 0 ≤ S ≤ 4 × 1010

บรรทัดที่สองมีจำนวนเต็ม p1 ถึง pN คั่นด้วยช่องว่าง โดยที่ 1 ≤ pi ≤ 109

ข้อมูลออก

จำนวนแบบของตะกร้าที่ราคารวมไม่เกิน S

4 10
3 5 7 2
11
3 0
4 1 6
1
ดูคำใบ้

N ถึง 40 ลองครบ 240 แบบไม่ทัน และตัดกิ่งก็ไม่ช่วย เพราะเมื่อ S ใหญ่ คำตอบเองก็ใหญ่ระดับ 1012 นับทีละแบบไม่ไหว ใช้ meet in the middle แบบหน้า 21.5 แบ่งของเป็นสองครึ่ง หาผลรวมทุกแบบของแต่ละครึ่ง ครึ่งละไม่เกิน 220 ค่า เรียงฝั่งขวา แล้วสำหรับผลรวม x ของฝั่งซ้ายแต่ละค่าที่ไม่เกิน S ให้นับว่าฝั่งขวามีกี่ค่าที่ไม่เกิน S - x ด้วย upper_bound ผลรวมกับคำตอบต้องเป็น long long ระวัง N เป็นเลขคี่ และอย่าลืมว่าตะกร้าว่างก็นับด้วย

ดูเฉลย
#include <bits/stdc++.h>
using namespace std;

vector<long long> all_sums(const vector<long long>& items)
{
    int m = items.size();
    vector<long long> sums(1 << m);
    for (int mask = 0; mask < (1 << m); mask++) {
        long long total = 0;
        for (int i = 0; i < m; i++) {
            if ((mask >> i) & 1) {
                total += items[i];
            }
        }
        sums[mask] = total;
    }
    return sums;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    long long budget;
    cin >> n >> budget;
    vector<long long> first_half, second_half;
    for (int i = 0; i < n; i++) {
        long long price;
        cin >> price;
        if (i < n / 2) {
            first_half.push_back(price);
        } else {
            second_half.push_back(price);
        }
    }
    vector<long long> left = all_sums(first_half);
    vector<long long> right = all_sums(second_half);
    sort(right.begin(), right.end());
    long long ways = 0;
    for (long long x : left) {
        if (x <= budget) {
            ways += upper_bound(right.begin(), right.end(), budget - x) - right.begin();
        }
    }
    cout << ways << '\n';
    return 0;
}