蓝书 0x00

0x01 位运算


AcWing89.a^b

快速幂模板题。

\(k\)\(b\)二进制下的位数,\(t_i\)\(b\)二进制下的第 \(i\) 位。

\(b = t_{k - 1} {2^{k - 1}} + t_{k - 2} 2 ^ {k - 2} + ... + t_02_0\)

于是 \(a ^ b = a^{t_{k - 1}{2^{k - 1}}} * a^{t_{k - 2}{2^{k - 2}}} * ... * a^{t_02_0}\)

所以我们只要将 \(b\) 二进制中所有为 \(1\) 的位 \(i\),取出,答案即为所有 \(a^{t_{i}2_i}\) 的乘积。

#include <iostream>
using namespace std;
typedef long long LL;
LL a, b, p;
int main() {
    scanf("%lld%lld%lld", &a, &b, &p);
    LL res = 1 % p;
    while (b) {
        if (b & 1) res = res * a % p;
        b >>= 1;
        a = a * a % p;
    }
    printf("%lld\n", res);
    return 0;
}

AcWing90. 64位整数乘法

思路类似快速幂。

\(b = t_{k - 1} {2^{k - 1}} + t_{k - 2} 2 ^ {k - 2} + ... + t_02_0\)

\(a * b = a \times t_{k - 1}{2^{k - 1}} + a \times t_{k - 2}{2^{k - 2} + ... + a \times t_02_0}\)

同样地,我们可以枚举每一个为 \(1\) 的二进制位,然后通过以上式子进行计算。

#include <iostream>
using namespace std;
typedef long long LL;
LL a, b, p;
int main() {
    cin >> a >> b >> p;
    LL res = 0;
    while (b) {
        if (b & 1) res = (res + a) % p;
        b >>= 1;
        a = (a + a) % p;
    } 
    cout << res << endl;
    return 0;
}

AcWing998. 起床困难综合症

由于给出的二进制运算的特点是不进位,所以每一位的计算是独立的。由于题目要求我们求出经过 \(n\) 次操作后答案最大的 \(x\) 并且 \(x \in [0,m]\),所以可以从高位到低位分别枚举 \(x\) 的每一位上填 \(0\) 还是填 \(1\)

具体的,\(x\) 的第 \(i\) 位可以填 \(1\) 当且仅当:

  1. 填上 \(1\)\(x <= m\)
  2. 该位填 \(1\) 优于该位填 \(0\)

首先第一种情况,我们只要判断当前 \(x + (1 << i)\) 是否小于 \(m\)
第二种情况,我们分别计算填 \(1\) 和 填 \(0\) 后的答案,比较即可确定该位的填法。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m, ans;
int t[N], op[N];

int work(int x, int j) {
    for (int i = 1; i <= n; i ++) {
        if (op[i] == 1) x &= t[i] >> j & 1;
        else if (op[i] == 2) x |= t[i] >> j & 1;
        else x ^= t[i] >> j & 1;
    }
    return x;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) {
        char s[10]; 
        scanf("%s %d", s, &t[i]);
        if (*s == 'A') op[i] = 1;
        else if (*s == 'O') op[i] = 2;
        else op[i] = 3;
    }
    for (int i = 30; i >= 0; i --) {
        if (1 << i <= m) {
            int x = work(0, i), y = work(1, i);
            if (x >= y) ans |= x << i;
            else ans |= y << i, m -= 1 << i; // 这里填 1 时直接用m减掉 1 << i,方便判断
        } else {
            ans |= work(0, i) << i;
        }
    }
    printf("%d\n", ans);
    return 0;
}

AcWing91. 最短Hamilton路径

状压 DP 入门题。

状态表示(\(f_{i,j}\))

  • 集合:当前经过点的状态为 \(i\),最后停在了 \(j\) 上。其中若 \(i\) 二进制的第 \(k\) 位为 \(1\) 则表示已经经过了第 \(k\) 个点,反之亦然。
  • 属性:\(\min\)

状态计算:考虑我们上一步是从 \(k\) 来到 \(j\) 的,那么我们的状态转移方程就应该是

\[f_{i,j} = \min{f_{i,j}, f_{i - 2 ^ j, k} + w_{k, j}} \]

初始化:\(f_{1, 0} = 0\),其余均为 \(INF\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 21, M = 1 << 21;
int n;
int f[M][N], w[N][N];
int main() {
    cin >> n;
    for (int i = 0; i < n; i ++)
        for (int j = 0; j < n; j ++)
            cin >> w[i][j];
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    for (int i = 0; i < 1 << n; i ++)
        for (int j = 0; j < n; j ++)
            if (i >> j & 1)
                for (int k = 0; k < n; k ++)
                    if (i >> k & 1) 
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
    cout << f[(1 << n) - 1][n - 1];
    return 0;
}

0x02 递推与递归


AcWing 95. 费解的开关

我们可以发现几个性质:

  1. 我们点灯的先后顺序不影响结果。
  2. 一个位置至多被点击一次
  3. 如果我们将第一行固定,那么想改变第一行就有且只有一种可能,便是操作该灯下方第二行的灯。

结合以上性质,我们可以进行递推,先固定第一行,然后操作第二行,接着再操作第三行,最后我们只要判断第五行操作完后是否为 \(1\) 即可。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n;
char g[5][5], backup[5][5];
void turn(int x, int y) {
    int dx[5] = {-1, 0, 0, 0, 1}, dy[5] = {0, -1, 0, 1, 0};
    for (int i = 0; i < 5; i ++) {
        int xx = x + dx[i], yy = y + dy[i];
        if (xx >= 0 && xx < 5 && yy >= 0 && yy < 5) 
            g[xx][yy] ^= 1;
    }
}
void solve() {
    int ans = 0x3f3f3f3f;
    for (int k = 0; k < 1 << 5; k ++) {
        int res = 0;
        memcpy(backup, g, sizeof g);
        for (int i = 0; i < 5; i ++)
            if (k >> i & 1) {
                res ++;
                turn(0, i);
            }
        for (int i = 0; i < 4; i ++)
            for (int j = 0; j < 5; j ++) 
                if (g[i][j] == '0') {
                    res ++;
                    turn(i + 1, j);
                }
        bool ok = true;
        for (int i = 0; i < 5; i ++)
            if (g[4][i] == '0') 
                ok = false;
        if (ok) ans = min(ans, res);
        memcpy(g, backup, sizeof backup);
    }
    if (ans > 6) ans = -1;
    printf("%d\n", ans);
}
int main() {
    int T; scanf("%d", &T);
    while (T --) {
        for (int i = 0; i < 5; i ++) scanf("%s", g[i]);
        solve();
    }
    return 0;
}

AcWing 96. 奇怪的汉诺塔

考虑 \(n\)\(3\) 塔的汉诺塔问题,令 \(d_i\) 表示 \(i\)\(3\) 塔问题的步数。我们考虑先将 \(i - 1\) 个盘子从 \(A\) 柱放到 \(B\) 柱,再把剩下的一个盘子放到 \(C\) 柱,最后再把 \(B\) 柱上的 \(i - 1\) 个盘子放到 \(C\) 柱上。

通过以上步骤,可以写出递推式 \(d_i = 2 \times d_{i - 1} + 1\)

接着考虑 \(4\) 塔问题。令 \(f_i\) 表示 \(i\)\(4\) 塔问题的步数。

我们可以先将 \(j\) 个盘子从 \(A\) 移到 \(B\),剩下的 \(n - j\) 个盘子做 \(3\) 塔问题,从 \(A\) 移到 \(D\),最后再把 \(B\) 柱上的 \(j\) 个盘子做 \(4\) 塔问题,移到 \(D\) 上。

类似的,我们可以写出递推式 \(f_i = \underset{1 \leq j < i}\min(2 \times f_{j} + d_{n - j})\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int n, f[N], d[N];
int main() {
    scanf("%d", &n);
    
    memset(f, 0x3f, sizeof f);
    f[1] = 1, d[1] = 1;
    for (int i = 2; i <= 12; i ++)
        for (int j = 1; j < i; j ++) {
            f[i] = min(f[i], 2 * f[j] + d[i - j]);
            d[i] = 2 * d[i - 1] + 1;
        } 
        
    for (int i = 1; i <= 12; i ++)
        cout << f[i] << endl;
    return 0;
}  

AcWing97. 约数之和

令 $A = p_1^{k_1}\times p_2^{k_2}\times ... \times p_n^{k_n} $

那么 \(A^B = p_1^{k_1B}\times p_2^{k_2B}\times ... \times p_n^{k_nB}\)

由约数之和的公式可得,\(A^B\) 的约数之和为 \((1 + p_1 + p_1^{2} + ... + p_1^{k_1B}) \times ... \times (1 + p_n + p_n ^ 2 + ... + p_n ^ {k_nB})\)

暴力循环计算以上式子肯定会超时,所以我们可以采用分治的思想。

我们考虑如何快速计算 \(1 + p + p ^ 2 + ... + p ^ n\)

首先,我们令 \(1 + p + p ^ 2 + ... + p ^ n = S(p, n)\)

接着我们根据 \(n\) 的奇偶性分两类讨论:

  1. \(n\) 为奇数

    $ S(p,n) = (1 + p + ... + p ^ {\frac{n - 1}{2}}) + (p ^ {\frac{n + 1}{2}} + ... + p ^ n) $

    \(=(1 + p + ... + p^{\frac{n - 1}{2}}) + p ^ {\frac{n + 1}{2}}(1 + p + ... + p^{\frac{n-1}{2}})\)

    \(=(1 + p^{\frac{n+1}{2}})(1 + p + ... + p^{\frac{n - 1}{2}})\)

    \(=(1 + p^{\frac{n+1}{2}})\times S(p, \frac{n - 1}{2})\)

  2. \(n\) 为偶数

    $ S(p,n) = (1 + p + ... + p^{\frac{n}{2} - 1}) + (p^{\frac{n}{2}} + ... + p^n)$

    \(= (1 + p + ... + p^{\frac{n}{2} - 1}) + p^{\frac{n}{2}}(1 + ... + p ^ {\frac{n}{2}})\)

    \(= (1 + p + ... + p^{\frac{n}{2} - 1}) + p^{\frac{n}{2}}(1 + ... + p ^ {\frac{n}{2} - 1}) + p^n\)

    \(= (p^{\frac{n}{2}} + 1)\times S(p, \frac{n}{2} - 1) + p^n\)

由此,我们便用 \(O(logn)\) 的时间计算出 \(S(p,n)\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int Mod = 9901;
typedef long long LL;
int a, b;

LL qmi(int a, int b) {
    a %= Mod;
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % Mod;
        b >>= 1;
        a = a * a % Mod;
    }
    return res;
}

LL work(int p, int n) {
    if (n == 0) return 1;
    if (n & 1) return (qmi(p, n + 1 >> 1) + 1) * work(p, n - 1 >> 1) % Mod;
    else return ((qmi(p, n >> 1) + 1) * work(p, (n >> 1) - 1) % Mod + qmi(p, n)) % Mod;
}

int main() {
    scanf("%d%d", &a, &b);
    
    LL res = 1;
    for (int i = 2; i <= a / i; i ++) {
        int cnt = 0;
        while (a % i == 0) a /= i, cnt ++;
        res = res * work(i, cnt * b) % Mod;
    }
    if (a > 1) res = res * work(a, b) % Mod;
    if (a == 0) res = 0;
    printf("%lld\n", res);
    return 0;
}

0x03 前缀和与差分

AcWing99. 激光炸弹

二维前缀和应用题。

枚举被炸区域的右下角,然后用二维前缀和计算 \((i - r + 1, j - r + 1)\) ~ \((i, j)\) 的总和,取最大值即可。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, M = 5010;
int n, r;
int g[M][M], s[M][M];
int main() {
    scanf("%d%d", &n, &r);
    for (int i = 1; i <= n; i ++) {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        s[x + 1][y + 1] += z;
    }
    int ans = -1;
    r = min(r, 5001);
    for (int i = 1; i <= 5001; i ++)
        for (int j = 1; j <= 5001; j ++)
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
    for (int i = r; i <= 5001; i ++) 
        for (int j = r; j <= 5001; j ++)
            ans = max(ans, s[i][j] - s[i - r][j] - s[i][j - r] + s[i - r][j - r]);
    printf("%d\n", ans);
    return 0;
}

AcWing100. 增减序列

观察题目,我们每次操作只会对某个区间加或减 \(1\),所以我们可以使用差分。

\(b\)\(a\) 的差分数组,即 \(b_i = a_i - a_{i - 1}\),这样对区间 \([l,r]\) 进行修改时只需要分别修改 \(b_l\)\(b_{r + 1}\) 即可。

因为要使 \(a\) 中的所有数都一样,所以 \(b_2\) ~ \(b_n\) 必须等于 \(0\),而 \(b_1\) 可以为任意数。

接下来考虑操作,一共四种情况:

  1. 选择 \(b_1\)\(b_{n+1}\)
  2. 选择 \(b_1\)\(b_{j}(j \leq n)\)
  3. 选择 \(b_j(j > 1)\)\(b_{n+1}\)
  4. 选择 \(b_i\)\(b_{j}\),其中 \(2\leq i,j\leq n\)

第一种情况无意义不考虑,剩下三种情况中,如果 \(b\) 中同时存在正数和负数,我们应该优先考虑第四种情况,然后才是考虑第二种情况和第三种情况。

所以我们设 \(b_2, b_3, ..., b_n\) 中有 \(p\) 个整数,\(q\) 个负数,那么最小操作次数就应该是 \(\min(p, q) + |p - q| = \max(p, q)\)

而在 \(|p - q|\) 次的操作过程中,我们可以选择第二种操作也可以选择第三种操作,所以可能得到的结果(也就是 \(b_1\) 的值)会有 \(|p - q| + 1\) 种。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
long long n, a[N], b[N];
long long p, q;
int main() {
    scanf("%lld", &n);
    for (int i = 1; i <= n; i ++) {
        scanf("%lld", &a[i]);
        b[i] = a[i] - a[i - 1];
    }
    for (int i = 2; i <= n; i ++)
        if (b[i] > 0) p += b[i];
        else q -= b[i];
    printf("%lld\n%lld\n", max(p, q), abs(p - q) + 1);
    return 0;
}

AcWing101. 最高的牛

注意到如果 \(a < b < c < d\),且\(a\)\(c\) 可以互相看到的话,\(b\)\(d\) 一定无法互相看到。

于是我们便可以用差分维护这个关系。具体的,如果 \(l\)\(r\) 可以互相看到的话,就让差分数组 \(b\) (初始都为 \(0\)) 中的 \(b_{l + 1} -1, b_{r} +1\),这样 \(l\)\(r\) 之间的牛的高度都会下降 \(1\)。然后对 \(b\) 求前缀和,求完后再给前缀和数组中的每一头牛的高度都加上 \(H\),也就是最高的牛的身高。

这种思想为 把对一个区间的操作转化成左右两端上的操作,再通过前缀和得到原问题的解

#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>
using namespace std;
const int N = 5010;
int n, p, h, m;
int b[N];
map<pair<int, int>, bool> H;
int main() {
    scanf("%d%d%d%d", &n, &p, &h, &m);
    for (int i = 1; i <= m; i ++) {
        int x, y;
        scanf("%d%d", &x, &y);
        if (x > y) swap(x, y);
        if (!H[make_pair(x, y)]) {
            H[make_pair(x, y)] = true;
            b[x + 1] --, b[y] ++;
        }
    }
    for (int i = 1; i <= n; i ++) b[i] += b[i - 1];
    for (int i = 1; i <= n; i ++) b[i] += h;
    for (int i = 1; i <= n; i ++) printf("%d\n", b[i]);
    return 0;
}

0x04 二分

AcWing102. 最佳牛围栏

简化题意为在数组 \(A\) 中找到一个长度不小于 \(L\) 的连续子段,且该子段平均值最大。

二分答案,将问题转换为判断平均值 \(x\) 是否合法,即是否存在长度不小于 \(L\) 的连续子段的平均值 \(\geq x\)

我们先把 \(A\) 中的每一个数都减去 \(x\),问题再次转换为是否存在长度不小于 \(L\) 的连续子段的总和 \(\geq 0\)

考虑前缀和,\(S_i = A_1 + ... + A_i\)。朴素地可以枚举子段的两端点,运用前缀和进行计算。

但这种做法一定会超时,我们考虑优化。

我们先把答案表示出来。

\(\underset{r - l \geq L}\max(A_{l + 1}, A_l + 1, ..., A_r) = \underset{L\leq r\leq n}\max(S_r - \underset{0\leq l \leq r - L}\min(S_l))\)

我们可以发现 \(r\) 每增加 \(1\)\(l\) 也只会增加 \(1\),所以我们不用每次都枚举 \(l\),可以维护 \(s_l\) 的最小值 \(minl\),然后判断当前的 \(s_r - minl\) 是否 \(\geq 0\) 即可。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, L;
int a[N];
double s[N];
bool check(double x) {
    for (int i = 1; i <= n; i ++) s[i] = a[i] - x;
    for (int i = 1; i <= n; i ++) s[i] += s[i - 1];
    double minn = 0x3f3f3f3f;
    for (int i = 0, j = L; j <= n; i ++, j ++) {
        minn = min(minn, s[i]);
        if (s[j] - minn >= 0) return true;
    }
    return false;
}
int main() {
    double l = 0, r = 0;
    scanf("%d%d", &n, &L);
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &a[i]);
        r = max(r, (double)(a[i]));
    }
    while (r - l > 1e-6) {
        double mid = (l + r) / 2;
        if (check(mid)) l = mid;
        else r = mid;
    }
    printf("%d\n", (int)(r * 1000));
    return 0;
}

AcWing113. 特殊排序

假设当前已经排好了 \(1,2,...,k\) 的顺序,考虑当前第 \(k+1\) 个元素应该插入的位置。

我们可以二分插入位置 \(mid\),如果 \(mid\)\(i\) 小,那么 \(l = mid\), 否则 \(r = mid - 1\)

可以证明如此二分一定能成功插入,证明如下:

如果第 \(k\) 个元素比第 \(mid\) 个元素小,那么我们可以寻找第 \(mid - 1\) 个元素。如果第 \(k\) 个元素比第 \(mid-1\) 个元素大,那么第 \(k\) 个元素就插入到 \(mid - 1\)\(mid - 2\) 之间;否则我们就继续往前找,直到第 \(1\) 个元素,如果比第 \(1\) 个元素小,那我们就插在第 \(1\) 个元素前面。

反之,若第 \(k\) 个元素大于第 \(mid\) 个元素,同上亦可证明。

虽然我们二分时不会像上方一个一个枚举,但也足够证明二分的正确性。

// Forward declaration of compare API.
// bool compare(int a, int b);
// return bool means whether a is less than b.

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int> res(1, 1);
        for (int i = 2; i <= N; i ++) {
            int l = 0, r = res.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (compare(res[mid], i)) l = mid;
                else r = mid - 1;
            }
            res.push_back(i);
            for (int j = res.size() - 2; j > r; j --) swap(res[j], res[j + 1]);
            if (compare(i, res[r])) swap(res[r], res[r + 1]);
        }
        return res;
    }
};

0x05 排序

AcWing103. 电影

使用 \(map\) 存储每种语言能使多少个人开心,接着把每台电影语言和字幕让人开心的数量存入结构体 \(v\) 中,接着以语言为主要,字幕为次要从大到小排序,v[1] 就是答案。

由于题目要求输出编号,所以还要加入 \(id\) 存储电影序号。

#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int N = 200010;
int n, m;
int a[N], b[N], c[N];
map<int, int> h;

struct node {
    int x, y, id;
} v[N];

bool cmp(node a, node b) {
    return a.x > b.x || a.x == b.x && a.y > b.y;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &a[i]);
        h[a[i]] ++;
    }
    scanf("%d", &m);
    for (int i = 1; i <= m; i ++) scanf("%d", &b[i]);
    for (int i = 1; i <= m; i ++) scanf("%d", &c[i]);
    for (int i = 1; i <= m; i ++) 
        v[i] = {h[b[i]], h[c[i]], i};
    sort(v + 1, v + 1 + m, cmp);
    printf("%d\n", v[1].id);
    return 0;
}

AcWing104. 货仓选址

超级经典的初中数学题。

现将 \(A\) 从小到大进行排序,设货仓建在 \(x\)\(x\) 左侧有 \(p\) 家店,右侧有 \(q\) 家店。

  1. 如果 \(p < q\),那么 \(x\) 每右移一个单位长度,距离之和就会变小 \(q - p\)
  2. 如果 \(p > q\),那么 \(x\) 每左移一个单位长度,距离之和就会变小 \(p - q\)

所以,我们应该将店设在最中间,即中位数。具体地,如果 \(n\) 是奇数,那货仓就建在 \(A_{\frac{n + 1}{2}}\) 处;如果 \(n\) 是偶数,那货仓就可以建在 \([A_\frac{n}{2}, A_\frac{n+1}{2}]\) 上的任意位置。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, a[N];
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n);
    int m = (1 + n) / 2;
    long long ans = 0;
    for (int i = 1; i <= n; i ++) ans += abs(a[i] - a[m]);
    printf("%d\n", ans);
    return 0;
}

AcWing105. 七夕祭

首先我们可以发现,我们只会交换相邻的两个数,所以我们可以把行和列分开来做。

接着考虑对于数组 \(a_1, a_2, ..., a_n\) 的最小交换次数。

先思考当前 \(1\)\(n\) 不能互相交换的情况。

由于最后 \(a\) 中的每个数都要变为 \(a\) 的平均值 \(\overline{x}\),所以我们记 \(c_i = a_i - \overline{x}\),然后我们用一个前缀和数组 \(S\),记录 \(c\) 的前缀和。

那么 \(\sum_{i = 1}^{n} |S_i|\) 即为答案。

为什么是这样的呢?我们可以举一个例子。

\(A: 1\ 3\ 2\ 7\ 7\)

\(C: -3 -1 -2\ 3\ 3\)

\(S: -3 -4 -6 -3\ 0\)

发现了吗?\(S_1\) 记录了 \(C_1\) 应该被 \(C_2\)\(3\),但是 \(C_2\) 本身就是 \(-1\),又要考虑给 \(C_1\) 的问题,所以 \(C_2\) 总共需要 \(C_3\) 给它 \(4\)\(C_3\) 同理,需要 \(C_4\)\(6\)。到了 \(C_4\),它本身拥有 \(3\),所以还需要 \(C_5\) 给它 \(3\)

这样子我们就可以不重不漏地计算出答案。

回到本题,从上述做法的线性改为了环形。注意到(注意力惊人)一定存在最优解不是环而是链,所以我们可以枚举环的断点,用链的做法去做。

证明如下:
来自 @AcWing 东边的西瓜皮

我们假设断点为 \(k\),那么原前缀和序列的顺序就成了 \(S_{k+1},...,S_n,S_1,...,S_k\),我们考虑修改过后的前缀和数组,即为 \(G\)

\(G_{k + 1} = S_{k + 1} - S_k\)

\(G_{k + 2} = S_{k + 2} - S_k\)

\(...\)

\(G_n = S_n - S_k\)

\(G_1 = S_1 + S_n - S_k\)

\(G_2 = S_2 + S_n - S_k\)

\(...\)

\(G_k = S_k + S_n - S_k\)

由于 \(S_n\)\(0\),所以 \(G_i = S_i - S_k\)

所以我们要找出使得 \(\sum_{i = 1}^{n}|S_i - S_k|\) 最小的 \(k\) 即可。这很明显是货仓选址问题,所以 \(k = \frac{(n + 1)}{2}\)

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m, T;
int row[N], col[N];
int a[N], b[N];
int main() {
    scanf("%d%d%d", &n, &m, &T);
    while (T --) {
        int x, y;
        scanf("%d%d", &x, &y);
        row[x] ++, col[y] ++;
    }
    int averow = 0, avecol = 0;
    for (int i = 1; i <= n; i ++) averow += row[i];
    for (int i = 1; i <= m; i ++) avecol += col[i];
    bool f1 = true, f2 = true;
    if (averow % n != 0) f1 = false;
    if (avecol % m != 0) f2 = false;
    if (!f1 && !f2) {
        printf("impossible");
        return 0;
    } else if (!f1) {
        printf("column ");
    } else if (!f2) {
        printf("row ");
    } else {
        printf("both ");
    }
    long long ans = 0;
    if (f1) {
        averow /= n;
        a[1] = 0;
        for (int i = 2; i <= n; i ++) a[i] = a[i - 1] + row[i] - averow;
        sort(a + 1, a + 1 + n);
        long long res = 0;
        for (int i = 1; i <= n; i ++) 
            res += abs(a[i] - a[n + 1 >> 1]);
        ans += res;
    }
    if (f2) {
        avecol /= m;
        b[1] = 0;
        for (int i = 2; i <= m; i ++) b[i] = b[i - 1] + col[i] - avecol;
        sort(b + 1, b + 1 + m);
        long long res = 0;
        for (int i = 1; i <= m; i ++) 
            res += abs(b[i] - b[m + 1 >> 1]);
        ans += res;
    }
    printf("%lld\n", ans);
    return 0;
}

AcWing106. 动态中位数

对顶堆算法。

用一个大根堆和一个小根堆来维护数据,做法是如果新加入的数据大于大根堆的堆顶,就把它加入小根堆,否则加入大根堆。

加入完后,我们还要对大根堆和小根堆进行维护,使得大根堆长度为小根堆长度加一。

我们只要在 \(n\) 为奇数时访问大根堆的堆顶即可。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int T, p, n;
int a[N];
int main() {
    scanf("%d", &T);
    while (T --) {
        scanf("%d%d", &p, &n);
        printf("%d %d\n", p, (n + 1) / 2);
        int cnt = 0;
        priority_queue<int> down;
        priority_queue<int, vector<int>, greater<int>> up;
        for (int i = 1; i <= n; i ++) {
            int x; scanf("%d", &x);
            if (down.empty() or x <= down.top()) down.push(x);
            else up.push(x);
            if (down.size() > up.size() + 1) up.push(down.top()), down.pop();
            if (up.size() > down.size()) down.push(up.top()), up.pop();
            if (i & 1) {
                printf("%d ", down.top());
                cnt ++;
                if (cnt % 10 == 0) puts("");
            }
        }
        if (cnt % 10) puts("");
    }
    return 0;
}

AcWing108. 奇数码问题

考虑将二维数组写成类似 \([1, 2, 3, 4, 5, 6, 7, 8]\) 的形式,则此时左右交换不会影响此序列。上下交换,比如交换 \(4\)\(7\),则会增加两个逆序对。我们可以拓展到 \(n\times n\) 的矩阵中,即上下交换会增加 \(n - 1\) 个逆序对。

因为 \(n\) 是奇数,所以 \(n - 1\) 一定是偶数,所以如果初始序列有奇数个逆序对,则不论我们怎么操作,最终都只有奇数个逆序对,反之亦然。

所以我们可以分别计算输入序列和答案序列的逆序对,判断它们的奇偶性是否相同。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>

using namespace std;

typedef long long LL;
const int N = 500010;
int n, q[N], tmp[N];

LL merge_sort(int l, int r)
{
    if (l >= r) return 0;
    int mid = l + r >> 1;
    LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++] = q[i ++];
        else 
        {
            tmp[k ++] = q[j ++];
            res += mid - i + 1;
        }
    while (i <= mid)
        tmp[k ++] = q[i ++];
    while (j <= r)
        tmp[k ++] = q[j ++];
    for (int i = l, j = 0; i <= r; i ++, j ++)
        q[i] = tmp[j];
    return res;
}

int main()
{
    while (cin >> n) {
        int len = 0;
        for (int i = 1; i <= n * n; i ++) {
            int x; cin >> x;
            if (x) q[len ++] = x;
        }
        LL res1 = merge_sort(0, n * n - 2);
        len = 0;
        for (int i = 1; i <= n * n; i ++) {
            int x; cin >> x;
            if (x) q[len ++] = x;
        }
        LL res2 = merge_sort(0, n * n - 2);
        if ((res1 & 1) == (res2 & 1)) puts("TAK");
        else puts("NIE");
    }
    return 0;
}

0x06 排序

AcWing 109.天才ACM

倍增。
最开始 \(l = 0\),然后进行如下操作:

  1. 定义变量 \(r = l, p = 1\)
  2. 循环判断 \([l, r + p)\) 的校验值是否小于等于 \(T\)
    • 如果是,r += p, p *= 2
    • 如果不是,p /= 2
  3. \(p = 0\) 时,退出循环,让 \(l = r\)

接着我们考虑如何判断校验值是否小于等于 \(T\)

一个显而易见的结论是,如果序列 \(A\) 是从小到大排序的,那么校验值就应该是 \((A_n - A_1) ^ 2 + (A_{n - 1} - A_2) ^ 2 + ...\)

即先让最大值和最小值凑一对,然后是次大和次小,并且我们配的对数量要小于 \(m\)

所以,假设当前要计算 \([l, r)\) 的最大值,我们只要将 \([l,r)\) 从小到大排序,做如上操作即可。

分析下时间复杂度,设每个答案的长度为 \(len_1,len_2,...,len_k\),那么对于每个 \(len_i\),需要倍增 \(O(logn)\)

那么总共的答案就是 \(O(logn \times len_1 log len_1 + logn \times len_2 log len_2 + ... + logn \times len_k log len_k)= O(logn(len_1loglen_1) + ... + len_kloglen_k)=O(n^2logn)\)

这个复杂度是一定会超时的,所以我们需要优化。

注意到在处理 \([l,r)\) 时,已经把 \([l, r)\) 排好了序,所以我们在处理 \([l, r + p)\) 时就不用
重新排序,只要将 \([r, r + p)\) 排序,然后和 \([l, r)\) 进行归并即可。

这种思路下只需要将每个区间处理一次,时间复杂度就是 \(O(len_1loglen_1 + ... + len_kloglen_k) \leq O(nlogn)\),可以通过本题。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 500010;
int T, n, m;
LL t;
int w[N], b[N], tmp[N];

bool check(int l, int mid, int r) {
    int k = 0;
    for (int i = mid; i < r; i ++) b[i] = w[i];
    sort(b + mid, b + r);
    int i = l, j = mid;
    while (i < mid && j < r)
        if (b[i] < b[j]) tmp[k ++] = b[i ++];
        else tmp[k ++] = b[j ++];
    while (i < mid) tmp[k ++] = b[i ++];
    while (j < r) tmp[k ++] = b[j ++];
    LL res = 0;
    for (int i = 0; i < m && i < k; i ++, k --)
        res += (LL)(tmp[k - 1] - tmp[i]) * (tmp[k - 1] - tmp[i]);
    return res <= t;
}

int main() {
    scanf("%d", &T);
    while (T --) {
        scanf("%d%d%lld", &n, &m, &t);
        for (int i = 0; i < n; i ++) scanf("%d", &w[i]);
        int l = 0, cnt = 0;
        while (l < n) {
            int r = l, p = 1;
            while (p) {
                if (r + p <= n && check(l, r, r + p)) {
                    r += p;
                    p <<= 1;
                    for (int i = l; i < r; i ++)
                        b[i] = tmp[i - l];
                } else {
                    p >>= 1;
                }
            }
            cnt ++;
            l = r;
        }
        printf("%d\n", cnt);
    }
    return 0;
}

0x07 贪心

AcWing1055. 股票买卖

考虑当前是第 \(i\) 天,分两种情况。

  1. 当前持有股票,则若下一天是涨,我们肯定会把股票留着,否则,我们应该直接卖出。
  2. 当前未持有股票,如果下一天涨,我们应该在今天买入,否则不应该买入。

所以可以得到 \(ans = \sum_{i = 2}^{n}\max(primes_i - primes_{i - 1}, 0)\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, w[N];
int f[N][2];
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);
    int res = 0;
    for (int i = 2; i <= n; i ++) 
        res += max(w[i] - w[i - 1], 0);
    printf("%d\n", res);
    return 0;
}

AcWing110. 防晒

首先把每一头牛可接受的光线按 \(minSPF\) 为第一关键字,\(maxSPF\) 为第二关键字,从大到小排序。

接着分别枚举每头牛可用的防晒霜,对于每头牛选择 \(SPF\) 最大的防晒霜。

证明:

首先,对于每一罐防晒霜 \(SPF_i\),因为我们已经按照 \(minSPF\) 进行了排序,所以如果
防晒霜 \(SPF_x\),大于 \(minSPF_{i}\),则它一定大于 \(minSPF_{i + 1}\) ~ \(minSPF_{n}\)
那么对于一头奶牛 \(i\) 与防晒霜 \(x\)\(y\) \((x < y)\) 来说,只有以下三种情况:

  1. \(x\) 能用,\(y\) 能用
  2. \(x\) 能用,\(y\) 不能用
  3. \(x\) 不能用,\(y\) 不能用

所以 \(x\) 的适用范围比 \(y\) 更广,故而我们应该尽可能先使用 \(y\),实在不行再考虑 \(x\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 2510;
int c, l;
PII w[N];
int spf[N], cov[N];
int main() {
    scanf("%d%d", &c, &l);
    for (int i = 1; i <= c; i ++) 
        scanf("%d%d", &w[i].first, &w[i].second);
    for (int i = 1; i <= l; i ++) 
        scanf("%d%d", &spf[i], &cov[i]);
    sort(w + 1, w + 1 + c, [&](PII a, PII b) {
        return a.first > b.first;
    });
    int ans = 0;
    for (int i = 1; i <= c; i ++) {
        int sel = 0;
        for (int j = 1; j <= l; j ++) {
            if (w[i].first <= spf[j] && spf[j] <= w[i].second && cov[j] && spf[j] > spf[sel])
                sel = j;
        }
        if (sel) ans ++, cov[sel] --;
    }
    printf("%d\n", ans);
    return 0;
}

AcWing111. 畜栏预定

首先对于每头牛,按照吃草的开始时间排序,然后从前到后考虑每只牛,用一个小根堆来枚举当前结束时间最早的畜栏。

对于一头牛,如果它吃草的开始时间比小根堆里所有的结束时间都早(即早于堆顶),就要新开辟一个畜栏。
否则,我们应该将其放入当前任意可放入的畜栏,方便起见,放入堆顶。

证明(来自 \(yxc\),非常精妙):

反证法,假设存在一种方案,使得需要的畜栏数量更少,记其需要的畜栏数量是 \(m\)
考虑在上述做法中,第一次新建第 \(m+1\) 个畜栏的时刻,不妨设当前处理的是第 \(i\)头牛。

由于所有牛是按开始时间从小到大排好序的,所以现在前 \(m\) 个畜栏中最后一头牛的开始时间一定小于等于第 \(i\)
头牛的开始时间。

而且前 \(m\) 个畜栏中最小的结束时间大于等于第 \(i\) 头牛的开始时间,所以前 \(m\)
个畜栏里最后一头牛的吃草区间一定都包含第 \(i\) 头牛的开始时间,所以我们就找到了 \(m+1\)
个区间存在交集,所以至少需要 \(m+1\) 个畜栏,矛盾。

所以上述做法可以得到最优解,证毕。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 50010;
int n, cnt;
int ans[N];
priority_queue<PII, vector<PII>, greater<PII>> q;
struct node {
    int l, r, id;
} w[N];
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) 
        scanf("%d%d", &w[i].l, &w[i].r);
    for (int i = 1; i <= n; i ++)
        w[i].id = i;
    sort(w + 1, w + 1 + n, [&](node a, node b) {
        return a.l < b.l;
    });
    int res = 0;
    for (int i = 1; i <= n; i ++) {
        if (q.empty() || w[i].l <= q.top().first) {
            q.push({w[i].r, ++ res});
            ans[w[i].id] = res;
        } else {
            PII t = q.top(); q.pop();
            q.push({w[i].r, t.second});
            ans[w[i].id] = t.second;
        }
    }
    printf("%d\n", res);
    for (int i = 1; i <= n; i ++) 
        printf("%d\n", ans[i]);
    return 0;
}

AcWing112. 雷达设备

如图,我们求出对于每个点 \((x, y)\),能覆盖它的范围 \([l,r]\),根据勾股定理我们可以得到:
\(l = y - \sqrt{D ^ 2 - x^2}, r = y + \sqrt{D^2 - x^2}\)

于是问题就转化成了,给定 \(n\) 个区间,在 \(x\) 上找尽量少的点,使每个区间内至少有一个点。

这就是 AcWing905. 区间选点,模板题。

我们把每个区间按 \(r\) 排序,对于每个区间:

  1. 如果区间内已经有了点,就跳过这个区间。
  2. 如果区间内没有点,就在区间的右端点找点。

正确性显然,不予证明。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef pair<double, double> PDD;
const int N = 1010;
int n, d;
PDD q[N];

PDD calc(int x, int y) {
    if (d * d < y * y) return {0x3f3f3f3f, 0x3f3f3f3f};
    double l = x - sqrt(d * d - y * y);
    double r = x + sqrt(d * d - y * y);
    return {l, r};
}

int main() {
    scanf("%d%d", &n, &d);
    for (int i = 1; i <= n; i ++) {
        int x, y;
        scanf("%d%d", &x, &y);
        q[i] = calc(x, y);
        if (q[i].first == 0x3f3f3f3f) {
            puts("-1");
            return 0;
        }
    }
    sort(q + 1, q + 1 + n, [&](PDD a, PDD b) {
        return a.second < b.second;
    });
    int cnt = 0;
    double ed = -0x3f3f3f3f;
    for (int i = 1; i <= n; i ++) {
        if (ed < q[i].first) {
            cnt ++;
            ed = q[i].second;
        } 
    }
    printf("%d\n", cnt);
    return 0;
}

AcWing114. 国王游戏

作法:把所有人按照 \(a \times b\) 从小到大排序,然后循环找最大值。

证明:

假设当前有两个人 \(i\)\(i + 1\),考虑如果排序前和排序后的答案。
这里假设国王为 \(0\) 号大臣。

  1. 排序前,即 \(a_i \times b_i > a_{i+1} \times b_{i+1}\)

    此时大臣 \(i\) 的奖赏是:\(\frac{\prod_{j = 0}^{i - 1}a_j}{b_i}\)

    此时大臣 \(i + 1\) 的奖赏是:\(\frac{\prod_{j = 0}^{i}a_j}{b_{i + 1}}\)

  2. 排序后,即 \(a_{i+1} \times b_{i+1} > a_i \times b_i\)

    此时大臣 \(i\) 的奖赏是:\(\frac{(\prod_{j = 0}^{i-1}a_j) \times a_{i + 1}}{b_i}\)

    此时大臣 \(i + 1\) 的奖赏是:\(\frac{\prod_{j = 0}^{i-1}a_j}{b_{i + 1}}\)

把每个式子都提取公因式 \(\prod_{j = 0}^{i - 1}a_j\),再乘上 \(b_i \times b_{i + 1}\)
转为求:\(\max(b_{i + 1}, a_i \times b_i)\)\(\max(b_i, a_{i + 1} \times b_{i + 1})\) 的最大值

通过推导可以得出,当 \(a_i \times b_i < a_{i + 1} \times b_{i + 1}\) 时,交换前更优;
反之,交换后更优。所以无论如何我们都应该按照 \(a_i \times b_i\) 排序。

要写高精度。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second
const int N = 1010, M = 4010;
int n;
PII w[N];

void mul(int a[], int b) {
    for (int i = 0, t = 0; i < M; i ++) {
        t += a[i] * b;
        a[i] = t % 10;
        t /= 10;
    }
}

void div(int c[], int a[], int b) {
    for (int i = M - 1, r = 0; i >= 0; i --) {
        r = r * 10 + a[i];
        c[i] = r / b;
        r %= b;
    }
}

int compare(int a[], int b[]) {
    for (int i = M - 1; i >= 0; i --)
        if (a[i] > b[i]) return 1;
        else if (a[i] < b[i]) return -1;
    return 0;
}

void print(int a[]) {
    int l = M - 1;
    while (a[l] == 0 && l > 0) l --;
    for (int i = l; i >= 0; i --)
        printf("%d", a[i]);
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i <= n; i ++)
        scanf("%d%d", &w[i].x, &w[i].y);
    sort(w + 1, w + 1 + n, [&](PII a, PII b) {
        return a.x * a.y < b.x * b.y;
    });
    int tmp[M], p[M] = {1}, res[M] = {0};
    for (int i = 1; i <= n; i ++) {
        mul(p, w[i - 1].x);
        div(tmp, p, w[i].y);
        if (compare(res, tmp) == -1) 
            memcpy(res, tmp, sizeof tmp);
    }
    print(res);
    return 0;
}

AcWing115. 给树染色

首先我们考虑没有限制的情况,那我们从大到小来取,现在有了限制,我们照样可以利用类似的思路。

假设当前最大值为 \(a\),它的父节点为 \(b\),那么 \(a\)\(b\) 取的顺序一定是相邻的,所以我们可以把它们当作一个点来做。

那么如果现在又有一个点 \(c\),那我们有两种染法:

  1. \(a,b\),再 \(c\)\(S_1 = a + 2b + 3c\)
  2. \(c\),再 \(a, b\)\(S_2 = c + 2a + 3b\)

我们考虑作差:\(S_1 - S_2 = a + 2b + 3c - c - 2a - 3b = 2c - (a + b) = 2(c - \frac{a + b}{2})\)

所以当 \(c>\frac{a+b}{2}\) 时 我们应该先染式子右边,否则先染式子左边。

我们考虑是否能把其拓展为两个序列。

假设有两个序列 \(a_1,a_2,...a_n\)\(b_1,b_2,...,b_m\)

与之相似我们也有两种染法:

  1. \(S_1 = \sum_{i = 1}^{n}a_i \times i + \sum_{i = n + 1}^{n + m}b_i \times i\)
  2. \(S_2 = \sum_{i = 1}^{m}b_i \times i + \sum_{i = m + 1}^{n + m}a_i \times i\)

作差:\(S_1 - S_2 = n \sum_{i = 1}^{m}b_i - m \sum_{i = 1}^{n}a_i\)

所以当 \(S_1 < S_2\) 时,\(\frac{\sum_{i = 1}^{m}b_i}{m} < \frac{\sum_{i = 1}^{n}a_i}{n}\)
,反之亦然。

因此,我们可以把 \(a\) 序列和 \(b\) 序列看作点权为 \(a\) 的平均值和 \(b\) 的平均值的两个点。

我们最后的做法是:每次寻找当前树上的最大值,然后找到它的父亲,在合并顺序中把最大值跟在父亲的后面,最后用最大值的点权更新父亲。

考虑如何计算这种合并方式的代价。

首先最开始先加上每个点的点权。对于当前要合并的 \(a\)\(b\),如果把 \(b\) 放在 \(a\) 的后面,就代表一整个 \(b\) 都要加上一个偏移量 \(k\)
由以上推导可以发现,偏移量其实就是 \(a\) 中点的数量,所以代价就应该加上 \(a\) 中点的数量乘以 \(b\) 中点权的总和。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, root;

struct Node {
    int p, s, size;
    double avg;
} nodes[N];

int find() {
    int res = 0;
    for (int i = 1; i <= n; i ++)
        if (i != root && nodes[i].avg > nodes[res].avg)
            res = i;
    return res;
}

int main() {
    int ans = 0;
    
    scanf("%d%d", &n, &root);
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &nodes[i].s);
        nodes[i].avg = nodes[i].s;
        nodes[i].size = 1;
        ans += nodes[i].s;
    }
    for (int i = 1; i < n; i ++) {
        int u, v;
        scanf("%d%d", &u, &v);
        nodes[v].p = u;
    }
    
    for (int i = 1; i < n; i ++) {
        int p = find();
        int fa = nodes[p].p;
        ans += nodes[fa].size * nodes[p].s;
        for (int j = 1; j <= n; j ++)
            if (nodes[j].p == p)
                nodes[j].p = fa;
        nodes[fa].size += nodes[p].size;
        nodes[fa].s += nodes[p].s;
        nodes[fa].avg = (double)nodes[fa].s / nodes[fa].size;
        nodes[p].avg = -1;
    } 
    printf("%d\n", ans);
    return 0;
}

习题

AcWing116. 飞行员兄弟

由于每个把手最多只可能进行一次操作,所以我们可以枚举每个把手是否操作。

为了方便枚举,我们可以把 \(+\) 当成 \(1\)\(-\) 当成 \(0\),然后二进制枚举 \(0\)\(2^{16} - 1\),然后判断该操作是否合法。

假设对于 \(0000\ 0100\ 0000\ 0000\)\(1\) 进行了操作,那么我们应该把第 \(2, 5, 6, 7, 8, 10, 14\) 位进行改变。

我们可以预处理出一个 \(row_i\) 表示对于第 \(i\) 行的所有数 \(k_1,k_2,k_3,k_4\),令 \(row_i = 2^{k_1} + 2^{k_2} + 2^{k_3} + 2^{k_4}\),同样的再预处理出维护列的 \(col_i\),那么如果我们对第 \(i\) 位进行了操作,那么我们只需要做 t ^= row[i / 4] + col[i % 4] - (1 << i) 即可。

时间复杂度为:\(O(2^{16} \times 16)\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10, M = 16;
int g, ans = 0x3f3f3f3f, ansg;
int row[N], col[N];

int check(int state) {
    int ans = 0, t = g;
    for (int i = 0; i < M; i ++) 
        if (state >> i & 1) {
            ans ++;
            int change = row[i / 4] + col[i % 4] - (1 << i);
            t ^= change;
        }
    for (int i = 0; i < M; i ++)
        if (t >> i & 1) ans = 0x3f3f3f3f;
    return ans;
}

int main() {
    for (int i = 0; i < 4; i ++)
        for (int j = 0; j < 4; j ++)
            row[i] |= (1 << (i * 4 + j));
    for (int j = 0; j < 4; j ++)
        for (int i = 0; i < 4; i ++)
            col[j] |= (1 << (i * 4 + j));
    for (int i = 0; i < 4; i ++)
        for (int j = 0; j < 4; j ++) {
            char c; cin >> c;
            if (c == '+') g |= 1 << (i * 4 + j);
        }
    for (int i = 0; i < 1 << M; i ++) {
        int r = check(i);
        if (ans > r) ans = r, ansg = i;
    }
    cout << ans << endl;
    for (int i = 0; i < M; i ++)
        if (ansg >> i & 1) 
            cout << i / 4 + 1 << ' ' << i % 4 + 1 << endl;
    return 0;
}

AcWing117. 占卜DIY

双端队列模拟,没什么好写的。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 15;
deque<int> q[15];
int cnt[N];
int main() {
    for (int i = 1; i <= 13; i ++) {
        for (int j = 1; j <= 4; j ++) {
            char c; cin >> c;
            if (c == 'J') q[i].push_back(11);
            else if (c == 'Q') q[i].push_back(12);
            else if (c == 'K') q[i].push_back(13);
            else if (c == 'A') q[i].push_back(1);
            else q[i].push_back(c == '0' ? 10 : c - '0');
        }
    }
    int die = 0, u = q[13].front();
    q[13].pop_front();
    while (die < 4) {
        if (u == 13) {
            die ++;
            u = q[13].front();
            q[13].pop_front();
            continue;
        }
        q[u].push_front(u);
        int now = u;
        u = q[u].back();
        q[now].pop_back();
        if (cnt[now] < 4) cnt[now] ++;
    }
    int res = 0;
    for (int i = 1; i <= 13; i ++)
        res += cnt[i] == 4;
    cout << res << endl;
    return 0;
}

AcWing118. 分形

找规律,我们可以发现第 \(i\) 层一共有 \(3^{i - 1}\) 行,并且可以用五个上一层的图形进行拼接而成。

我们找到每个图形的左上角坐标,分别为 \((1, 1),(1, 3^{i - 2}\times 2 + 1),(3^{i - 2} + 1,3^{i - 2} + 1),(3^{i - 2}\times 2 + 1, 1), (3^{i - 2}\times 2 + 1, 3^{i - 2}\times 2 + 1)\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 800;
char g[N][N][8];

int pow3(int x) {
    int res = 1;
    for (int i = 1; i <= x; i ++) res *= 3;
    return res;
}

void work(int k, int x1, int y1) {
    for (int i = 1; i <= pow3(k - 2); i ++)
        for (int j = 1; j <= pow3(k - 2); j ++)
            g[x1 + i - 1][y1 + j - 1][k] = g[i][j][k - 1];
}

int main() {
    for (int k = 1; k <= 7; k ++)
        for (int i = 1; i <= pow3(k - 1); i ++)
            for (int j = 1; j <= pow3(k - 1); j ++)
                g[i][j][k] = ' ';
    g[1][1][1] = 'X';
    
    for (int i = 2; i <= 7; i ++) {
        work(i, 1, 1);
        work(i, 1, pow3(i - 2) * 2 + 1);
        work(i, pow3(i - 2) + 1, pow3(i - 2) + 1);
        work(i, pow3(i - 2) * 2 + 1, 1);
        work(i, pow3(i - 2) * 2 + 1, pow3(i - 2) * 2 + 1);
    }   
    
    int n;
    while (cin >> n && ~n) {
        for (int i = 1; i <= pow3(n - 1); i ++, puts(""))
            for (int j = 1; j <= pow3(n - 1); j ++) 
                printf("%c", g[i][j][n]);
        puts("-");
    }
    return 0;
}

AcWing119. 袭击

如上图,我们可以把区间根据 \(x\) 分为两个部分,这样我们只要能够求出从 \([l,mid]\) 中选出一个点和从 \([mid + 1, r]\) 中选出一个点的最小距离。

首先,我们令当前最小距离 \(res = \min(dfs(l, mid), dfs(mid + 1, r))\),如下图

我们只需要求出阴影范围内的点即可。

这里有一个性质:对于一个左边的点,右边最多只有 \(6\) 个点会被考虑。

证明:如下图。

假设当前有 \(7\) 个点被考虑,那么根据抽屉原理,一定会有两个点在同一个格子里。我们发现大长方形的宽为 \(r\),长为 \(2r\),所以小长方形的长为 \(\frac{2}{3}r\),宽为 \(\frac{1}{2}r\),那么在长方形内部的最长距离也就是对角线长度为 \(\sqrt{(\frac{2}{3}r)^2 + (\frac{1}{2}r) ^ 2} = \frac{5}{6}r < r = ans\),所以不可能有 \(7\) 个点。

所以我们每次枚举会枚举 \(6\) 个点,复杂度为 \(O(6n)\),总复杂度为 \(O(nlogn)\)

由于一些奇怪的有序的数据会使我们的 \(sort\) 退化成 \(O(n^2)\),所以我们要在排序前先打乱一下数组。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 200010, INF = 0x3f3f3f3f;
int T, n, m;
double ans;

struct node {
    double x, y;
    int type;
} points[N], tmp[N];

double dist(node a, node b) {
    if (a.type == b.type) return INF;
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

double dfs(int l, int r) {
    if (l == r) return INF;
    int mid = l + r >> 1;
    double midx = points[mid].x;
    double res = min(dfs(l, mid), dfs(mid + 1, r));
    
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r) 
        if (points[i].y < points[j].y) tmp[k ++] = points[i ++];
        else tmp[k ++] = points[j ++];
    while (i <= mid) tmp[k ++] = points[i ++];
    while (j <= r) tmp[k ++] = points[j ++];
    for (int i = l; i <= r; i ++) points[i] = tmp[i - l];
    
    k = 0;
    for (int i = l; i <= r; i ++)
        if (points[i].x >= midx - res && points[i].x <= midx + res)
            tmp[k ++] = points[i];
    
    for (int i = 0; i < k; i ++)
        for (int j = i - 1; j >= 0 && tmp[i].y - tmp[j].y < res; j --)
            res = min(res, dist(tmp[i], tmp[j]));
    ans = min(ans, res);    
    return res;
}

int main() {
    scanf("%d", &T);
    while (T --) {
        scanf("%d", &n);
        for (int i = 0; i < n; i ++) {
            scanf("%lf%lf", &points[i].x, &points[i].y);
            points[i].type = 0;
        }
        for (int i = n; i < n * 2; i ++) {
            scanf("%lf%lf", &points[i].x, &points[i].y);
            points[i].type = 1;
        }
        n *= 2;
        random_shuffle(points, points + n);
        sort(points, points + n, [&](node a, node b){
            return a.x < b.x;
        });
        ans = dist(points[0], points[n - 1]);
        double res = dfs(0, n - 1);
        printf("%.3lf\n", res);
    }
    return 0;
}

AcWing120. 防线

考虑到防线上最多只有一个奇数防具,所以我们可以二分,如果 \([l,mid]\) 的和为奇数,就代表奇数在这段区间内 \(r = mid\),否则 \(l = mid + 1\)

那么如何快速计算 \([l,mid]\) 的和呢?我们可以用到前缀和思想。

考虑如何计算 \([mins, x]\) 之间的和。

我们可以枚举每一个等差数列 \(p[i]\),如果 \(p[i].s \leq x\) 说明两个区间存在交集,那么交集应该为 \([p[i].s, min(p[i].e, x)]\)

根据小学奥数,交集内部的数量就应该为(末项-首项)/ 公差 + 1,即 \((min(p[i].e,x) - p[i].s) \div p[i].d + 1\)

所以 \([l, mid]\) 的和就等于 \(calc(mins, mid) - calc(mins, l)\)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 200010;
int T, n;
int s[N];

struct node {
    int s, e, d;
} p[N];

int calc(int x) {
    int res = 0;
    for (int i = 1; i <= n; i ++) 
        if (p[i].s <= x) 
            res += (min(p[i].e, x) - p[i].s) / p[i].d + 1;
    return res;
}

int main() {
    scanf("%d", &T);
    while (T --) {
        int l = 0x3f3f3f3f, r = -0x3f3f3f3f;
        scanf("%d", &n);
        for (int i = 1; i <= n; i ++) {
            scanf("%d%d%d", &p[i].s, &p[i].e, &p[i].d);
            l = min(l, p[i].s);
            r = max(r, p[i].e);
        }
        
        if (calc(r) & 1) {
            while (l < r) {
                int mid = l + r >> 1;
                if ((calc(mid) - calc(l - 1)) & 1) r = mid;
                else l = mid + 1;
            }
            printf("%d %d\n", l, calc(l) - calc(l - 1));
        } else {
            puts("There's no weakness.");
        }
    }
    return 0;
}

AcWing121. 赶牛入圈

先把 \(X,Y\) 坐标离散化,然后计算离散化后的前缀和。接着我们考虑二分正方形边长,该二分正确性显然,因为在满足条件的情况下边长变大答案不可能更优,而不满足条件时边长一定会增大。

考虑如何写 \(check\) 函数。

由于我们已经预处理好了前缀和,我们就可以枚举正方形的左上角和右下角,然后通过前缀和来计算答案。一个细节是,因为四叶草的坐标是格子的左下角,所以我们在计算是要用 \(x1 - x0 + 1\)\(y1 - y0 + 1\)(建议画图)。

一个更细节的细节是,我们没必要一定枚举正方形,我们发现如果一个长方形的长和宽都小于等于正方形的边长,并且长方形内的四叶草数量大于等于 \(c\),那正方形情况也一定成立。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second

const int N = 1010;
int c, n;
int s[N][N];
PII p[N];
vector<int> nums;

int find(int x) {
    int l = 0, r = nums.size() - 1;
    while (l < r) {
        int mid = l + r >> 1;
        if (nums[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}

bool check(int l) {
    for (int x0 = 0, x1 = 1; x1 < nums.size(); x1 ++) {
        while (nums[x1] - nums[x0 + 1] + 1 > l) x0 ++;
        for (int y0 = 0, y1 = 1; y1 < nums.size(); y1 ++) {
            while (nums[y1] - nums[y0 + 1] + 1 > l) y0 ++;
            if (s[x1][y1] - s[x1][y0] - s[x0][y1] + s[x0][y0] >= c)
                return true;
        }
    }
    return false;
}

int main() {
    scanf("%d%d", &c, &n);
    nums.push_back(0);
    for (int i = 1; i <= n; i ++) {
        scanf("%d%d", &p[i].x, &p[i].y);
        nums.push_back(p[i].x);
        nums.push_back(p[i].y);
    }
    sort(nums.begin(), nums.end());
    nums.erase(unique(nums.begin(), nums.end()), nums.end());
    
    for (int i = 1; i <= n; i ++) {
        int x = find(p[i].x), y = find(p[i].y);
        s[x][y] ++;
    }
    for (int i = 1; i < nums.size(); i ++)
        for (int j = 1; j < nums.size(); j ++)
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
    
    int l = 1, r = 10000;
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    
    printf("%d\n", l);
    return 0;
}

AcWing122. 糖果传递

七夕祭缩减版。推导过程不再写了。

#include <iostream>
#include <cstring>
#include <algorithm>
typedef long long LL;
using namespace std;
const int N = 1000010;
int n, a[N], w[N], s[N];
int main() {
    scanf("%d", &n);
    LL avg = 0;
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &a[i]);
        avg += a[i];
    }
    avg /= n;
    for (int i = 1; i <= n; i ++)
        w[i] = a[i] - avg;
    for (int i = 1; i <= n; i ++)
        s[i] = s[i - 1] + w[i];
    sort(s + 1, s + 1 + n);
    int k = n + 1 >> 1;
    long long res = 0;
    for (int i = 1; i <= n; i ++)
        res += abs(s[i] - s[k]);
    printf("%lld\n", res);
    return 0;
}

AcWing123. 士兵

很容易发现我们可以把 \(x\)\(y\) 分开来做。

首先考虑 \(y\) 轴,排序后就是货仓选址。

再考虑 \(x\) 轴,此处有一个性质是最优情况下,排序后士兵间的相对顺序和最后走到最终位置的相对顺序不会改变。

比如最左边的点走到 \(p_1 = k\),则第 \(i\) 个人走到 \(p_i = k + (i - 1)\)

所以第 \(i\) 个士兵(设其坐标为 \(x_i\))要走的距离就是 \(|x_i - [(k + (i - 1))]| = |x_i - (i - 1) - k|\)

所以问题又变为了货仓选址,我们只需要给每个 \(x_i - i\) 即可。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second

const int N = 10010;
int n, c[N];
PII p[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        scanf("%d%d", &p[i].x, &p[i].y);
    sort(p + 1, p + 1 + n, [&](PII a, PII b) {
        return a.y < b.y;
    });
    int k = p[n + 1 >> 1].y;
    int res = 0;
    for (int i = 1; i <= n; i ++)
        res += abs(p[i].y - k);
    sort(p + 1, p + 1 + n, [&](PII a, PII b) {
        return a.x < b.x;
    });
    for (int i = 1; i <= n; i ++) 
        p[i].x -= i - 1;
    sort(p + 1, p + 1 + n, [&](PII a, PII b) {
        return a.x < b.x;
    });
    k = p[n + 1 >> 1].x;
    for (int i = 1; i <= n; i ++) {
        res += abs(p[i].x - k);
    }
    printf("%d\n", res);
    return 0;
}

AcWing124. 数的进制转换

高精度 + 模拟进制转换,没什么好写的,注意细节就行了。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500;
int n, m, T;
string s, t;

int get(char c) {
    if (c >= '0' && c <= '9') return c - '0';
    else if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
    else return c - 'a' + 36;
}

char reget(int c) {
    if (c <= 9) return c + '0';
    else if (c < 36) return c  - 10 + 'A';
    else return c - 36 + 'a';
}

vector<int> div(vector<int> A, int b, int &r) {
    vector<int> C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i --) {
        r = r * n + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.back() == 0 && C.size() > 0) C.pop_back();
    return C;
}

int main() {
    cin >> T;
    while (T --) {
        cin >> n >> m >> s;
        cout << n << ' ' << s << endl << m << ' ';
        vector<int> res, ans;
        for (int i = s.size() - 1; i >= 0; i --)
            res.push_back(get(s[i]));
        while (res.size()) {
            int r;
            res = div(res, m, r);
            ans.push_back(r);
        }
        for (int i = ans.size() - 1; i >= 0; i --)
            cout << reget(ans[i]);
        cout << endl << endl;
    }
    return 0;
}

AcWing125. 耍杂技的牛

\(s + w\) 进行排序,证明类似国王游戏,这里不过多赘述。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 50010;
int n;

struct node {
    int s, w;
} a[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        scanf("%d%d", &a[i].w, &a[i].s);
    sort(a + 1, a + 1 + n, [&](node a, node b) {
        return a.w + a.s < b.w + b.s;
    });
    long long res = -0x3f3f3f3f, sum = 0;
    for (int i = 1; i <= n; i ++) {
        res = max(res, sum - a[i].s);
        sum += a[i].w;
    } 
    printf("%d\n", res);
    return 0;
}

AcWing126. 最大的和

我们先预处理矩阵的前缀和,朴素思路是枚举矩阵的左上角和右下角,这样时间复杂度是 \(O(n^4)\)

我们考虑优化,只枚举矩阵的起始行和结尾行,即 \(x_1\)\(x_2\),然后用类似最长连续子段和的思路从 \(1\) 开始枚举列,找到最大连续列的和。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, g[N][N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++) {
            scanf("%d", &g[i][j]);
            g[i][j] += g[i - 1][j];
        }
    
    int res = -0x3f3f3f3f;
    for (int i = 1; i <= n; i ++)
        for (int j = i; j <= n; j ++) {
            int last = 0;
            for (int k = 1; k <= n; k ++) {
                last = max(last, 0) + g[j][k] - g[i - 1][k];
                res = max(res, last);
            }
        }
    
    printf("%d\n", res);
    return 0;
}

AcWing127. 任务

我们发现时间对收入的影响比级别多的多的多,所以我们以时间为第一关键字,收入为第二关键字从大到小排序任务和机器。

我们从 \(1\)\(n\) 枚举每个任务,接着寻找每个可以使用的机器,并且使用其中级别最小的机器。

因为任务排过序,所以如果一台机器能满足当前任务的时间,就一定能满足之后所有任务的时间,但级别却不一定满足,所以我们需要使用可行机器里级别最小的。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <set>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m;

struct node {
    int x, y;
} a[N], b[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++)
        scanf("%d%d", &a[i].x, &a[i].y);
    for (int i = 1; i <= m; i ++)
        scanf("%d%d", &b[i].x, &b[i].y);

    sort(a + 1, a + 1 + n, [&](node a, node b) {
        return a.x > b.x || a.x == b.x && a.y > b.y;
    });
    sort(b + 1, b + 1 + m, [&](node a, node b) {
        return a.x > b.x || a.x == b.x && a.y > b.y;
    });

    multiset<int> S; 
    LL res = 0, cnt = 0;
    for (int i = 1, j = 1; i <= m; i ++) {
        while (j <= n && b[i].x <= a[j].x) 
            S.insert(a[j ++].y);
        auto it = S.lower_bound(b[i].y);
        if (it != S.end()) {
            cnt ++;
            res += b[i].x * 500 + b[i].y * 2;
            S.erase(it);
        }
    }
    printf("%lld %lld\n", cnt, res);
    return 0;
}
posted @ 2024-12-15 11:04  はなこくん  阅读(31)  评论(0)    收藏  举报