跳转至

250716 模拟赛

T3 nuclear

有一排 \(n\) 个空位,每个位置可以放一个物品或者不放,要求不能连续放 \(c\) 个物品,问方案数。

\(n\le 10^{18},\ c\le 500\)

用 DP 容易做到 \(O(n)\),并且可以用矩阵快速幂优化到 \(O(c^3\log n)\),不能通过。

注意到 \(c\) 很小,但 \(n\) 很大,考虑另一些枚举 \(c\) 并且包含 \(\log n\) 的做法。我们将当前长为 \(n\) 的问题劈成两半 \(L,R\)\(L+R=n\)),然后处理分割线附近的情况。我们可以枚举分割线左、右两侧分别有连续多少个物品,记为 \(i,j\),它们需要满足 \(i+j\le c\)\(L,R\) 分别去掉 \(i+1\)\(j+1\) 之后剩下的部分变为子问题递归处理。

考虑这样做的时间复杂度。生成的 \(O(c)\) 个子问题 \(n'\) 都满足 \(n'\in [\frac 12 n-c,\frac 12 n]\)(分析时间复杂度时不细扣边界);下一层递归之后生成的子问题 \(n''\) 都满足 \(n''\in [\frac 12 n'-c,\frac 12 n']=[\frac 14 n-\frac 32 c,\frac 14 n]\);注意到区间长度始终不会超过 \(2c\),同时最多 \(\log n\) 层,因此本质不同子问题数量为 \(O(c\log n)\) 级别。

由于转移时还需要枚举 \(c\)(这里不要暴力枚举 \((i,j)\),使用前缀和优化即可),因此总时间复杂度为 \(O(c^2\log n)\)。再将 \(n\le 10^6\) 直接预处理出来,可以更快(约 \(\frac 13\))。

代码
#include<iostream>
#include<unordered_map>
#include<cstring>
#define ll long long
using namespace std;
const int MOD = 1e9 + 7;

inline void add(int &a, int b) { a = (a + b) % MOD; }

ll n;
int c;

struct hash_table {
    static const int MOD = 1e7 + 19;
    struct Node {
        ll key;
        int v, next;
    } pool[64 * 510 * 510];
    int nn, head[MOD];
    inline hash_table() : nn(0) { memset(head, 0, sizeof(head)); }
    int &insert(ll key) {
        pool[++nn] = {key, 0, head[key % MOD]};
        head[key % MOD] = nn;
        return pool[nn].v;
    }
    int &operator[](ll key) {
        for(int i = head[key % MOD]; i; i = pool[i].next) {
            if(pool[i].key == key) return pool[i].v;
        }
        return insert(key);
    }
} mp;

namespace baoli {

    const int N = 1e6 + 10;

    int f[N];

    void work(int tp = 1, int _n = n) {
        int s = 1;
        f[-1 + 2] = 1;
        f[0 + 2] = 1;
        for(int i = 1; i <= _n; i++) {
            if(i - c - 2 >= -1) s = (s - f[i - c - 2 + 2] + MOD) % MOD;
            s = (s + f[i - 1 + 2]) % MOD;
            f[i + 2] = s;
        }
        if(tp) cout << f[_n + 2] << '\n';
    }

}

namespace c2logv {

    using baoli::f;

    int dfs(ll n) {
        if(n < -1) return 0;
        if(n <= 0) return 1;
        if(n <= 1) return 2;
        if(n <= 1000000) return f[n];
        int &res = mp[n];
        if(res) return res;
        ll l = n / 2, r = n - l;
        int sum = 0;
        for(int i = c; i >= 0; i--) {
            add(sum, dfs(r - (c - i) - 1));
            add(res, (ll)sum * dfs(l - i - 1) % MOD);
        }
        return res;
    }

    void work() {
        if(c == 0) { cout << 1 << '\n'; return; }
        baoli::work(0, 1000000);
        for(int i = 1; i <= 1000000; i++) f[i] = f[i + 2];
        cout << dfs(n) << '\n';
    }

}

// #define FIO

signed main() {

    #ifdef FIO
    freopen("nuclear.in", "r", stdin);
    freopen("nuclear.out", "w", stdout);
    #endif

    cin >> n >> c;

    // 原题还需要数据点分治
    if(n <= 1000000) baoli::work();
    else c2logv::work();

    return 0;
}

T4 game

\(n\) 个物品,A 和 B 轮流取物品,A 先手。每个人每回合至少取一个物品,至多取上一个人上一次取物品数的两倍。将最后一个物品拿走的人获胜。A 在第一回合可以取任意物品。问 A 第一回合至少取多少物品可以胜利(假设 A,B 足够聪明)。

\(n\le 10{18}\)

先考虑暴力,设 \(f[i][j]\) 表示还剩 \(i\) 个物品,上一次拿了 \(j\) 个物品,是否先手必胜。容易做到 \(O(n^2)\)

暴力
#include<iostream>
using namespace std;
const int N = 10010;

int n;
bool f[N][N];

int main() {

    cin >> n;
    if(n > 10010) return 0;

    for(int i = 1; i <= n; i++) f[1][i] = 1;

    for(int i = 2; i <= n; i++) {
        bool tmp = 0;
        for(int j = 1; j <= n; j++) {
            if(2 * j - 1 <= i) tmp = tmp || !f[i - (2 * j - 1)][2 * j - 1];
            if(2 * j <= i) tmp = tmp || !f[i - (2 * j)][2 * j];
            f[i][j] = tmp;
        }
    }

    for(int i = 1; i <= n; i++) {
        if(!f[n - i][i]) { cout << i << '\n'; break; }
    }

    return 0;
}

然后打出 \(n\in[1,100]\) 的表:

for n in {1..100}
do  
    echo -e "$n \c"
    echo $n | ./a
done
1 1
2 2
3 3
4 1
5 5
6 1
7 2
8 8
9 1
10 2
11 3
12 1
13 13
14 1
15 2
16 3
17 1
18 5
19 1
20 2
21 21
22 1
23 2
24 3
25 1
26 5
27 1
28 2
29 8
30 1
31 2
32 3
33 1
34 34
35 1
36 2
37 3
38 1
39 5
40 1
41 2
42 8
43 1
44 2
45 3
46 1
47 13
48 1
49 2
50 3
51 1
52 5
53 1
54 2
55 55
56 1
57 2
58 3
59 1
60 5
61 1
62 2
63 8
64 1
65 2
66 3
67 1
68 13
69 1
70 2
71 3
72 1
73 5
74 1
75 2
76 21
77 1
78 2
79 3
80 1
81 5
82 1
83 2
84 8
85 1
86 2
87 3
88 1
89 89
90 1
91 2
92 3
93 1
94 5
95 1
96 2
97 8
98 1
99 2
100 3

发现当 \(n\) 是斐波那契数的时候,其答案是其本身;其余情况,答案也都是斐波那契数。猜测跟斐波那契数有关

发现两个斐波那契数中间的答案和开头的答案完全相同,不难想到是删除了比 \(n\) 小的最大的斐波那契数,直到 \(n\) 是斐波那契数。

代码
// 比暴力还短
#include<iostream>
#define ll long long
using namespace std;

ll n, m;

ll fib[100];

// #define FIO

int main() {

    #ifdef FIO
    freopen("game.in", "r", stdin);
    freopen("game.out", "w", stdout);
    #endif

    cin >> n;

    fib[0] = fib[1] = 1;
    for(int i = 2; i <= 100; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
        if(fib[i] > n) { m = i; break; }
    }

    for(int i = m; i >= 0; i--) {
        if(n == fib[i]) {
            cout << n << '\n';
            break;
        } else if(n > fib[i]) {
            n -= fib[i];
        }
    }

    return 0;
}