蓝书 0x00

0x01 位运算


AcWing89.a^b

快速幂模板题。

kb二进制下的位数,tib二进制下的第 i 位。

b=tk12k1+tk22k2+...+t020

于是 ab=atk12k1atk22k2...at020

所以我们只要将 b 二进制中所有为 1 的位 i,取出,答案即为所有 ati2i 的乘积。

#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=tk12k1+tk22k2+...+t020

ab=a×tk12k1+a×tk22k2+...+a×t020

同样地,我们可以枚举每一个为 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[0,m],所以可以从高位到低位分别枚举 x 的每一位上填 0 还是填 1

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

  1. 填上 1x<=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 入门题。

状态表示(fi,j)

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

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

fi,j=minfi,j,fi2j,k+wk,j

初始化:f1,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. 奇怪的汉诺塔

考虑 n3 塔的汉诺塔问题,令 di 表示 i3 塔问题的步数。我们考虑先将 i1 个盘子从 A 柱放到 B 柱,再把剩下的一个盘子放到 C 柱,最后再把 B 柱上的 i1 个盘子放到 C 柱上。

通过以上步骤,可以写出递推式 di=2×di1+1

接着考虑 4 塔问题。令 fi 表示 i4 塔问题的步数。

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

类似的,我们可以写出递推式 fi=min1j<i(2×fj+dnj)

#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=p1k1×p2k2×...×pnkn

那么 AB=p1k1B×p2k2B×...×pnknB

由约数之和的公式可得,AB 的约数之和为 (1+p1+p12+...+p1k1B)×...×(1+pn+pn2+...+pnknB)

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

我们考虑如何快速计算 1+p+p2+...+pn

首先,我们令 1+p+p2+...+pn=S(p,n)

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

  1. n 为奇数

    S(p,n)=(1+p+...+pn12)+(pn+12+...+pn)

    =(1+p+...+pn12)+pn+12(1+p+...+pn12)

    =(1+pn+12)(1+p+...+pn12)

    =(1+pn+12)×S(p,n12)

  2. n 为偶数

    S(p,n)=(1+p+...+pn21)+(pn2+...+pn)

    =(1+p+...+pn21)+pn2(1+...+pn2)

    =(1+p+...+pn21)+pn2(1+...+pn21)+pn

    =(pn2+1)×S(p,n21)+pn

由此,我们便用 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. 激光炸弹

二维前缀和应用题。

枚举被炸区域的右下角,然后用二维前缀和计算 (ir+1,jr+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,所以我们可以使用差分。

ba 的差分数组,即 bi=aiai1,这样对区间 [l,r] 进行修改时只需要分别修改 blbr+1 即可。

因为要使 a 中的所有数都一样,所以 b2 ~ bn 必须等于 0,而 b1 可以为任意数。

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

  1. 选择 b1bn+1
  2. 选择 b1bj(jn)
  3. 选择 bj(j>1)bn+1
  4. 选择 bibj,其中 2i,jn

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

所以我们设 b2,b3,...,bn 中有 p 个整数,q 个负数,那么最小操作次数就应该是 min(p,q)+|pq|=max(p,q)

而在 |pq| 次的操作过程中,我们可以选择第二种操作也可以选择第三种操作,所以可能得到的结果(也就是 b1 的值)会有 |pq|+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,且ac 可以互相看到的话,bd 一定无法互相看到。

于是我们便可以用差分维护这个关系。具体的,如果 lr 可以互相看到的话,就让差分数组 b (初始都为 0) 中的 bl+11,br+1,这样 lr 之间的牛的高度都会下降 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 的连续子段的平均值 x

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

考虑前缀和,Si=A1+...+Ai。朴素地可以枚举子段的两端点,运用前缀和进行计算。

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

我们先把答案表示出来。

maxrlL(Al+1,Al+1,...,Ar)=maxLrn(Srmin0lrL(Sl))

我们可以发现 r 每增加 1l 也只会增加 1,所以我们不用每次都枚举 l,可以维护 sl 的最小值 minl,然后判断当前的 srminl 是否 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,如果 midi 小,那么 l=mid, 否则 r=mid1

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

如果第 k 个元素比第 mid 个元素小,那么我们可以寻找第 mid1 个元素。如果第 k 个元素比第 mid1 个元素大,那么第 k 个元素就插入到 mid1mid2 之间;否则我们就继续往前找,直到第 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 从小到大进行排序,设货仓建在 xx 左侧有 p 家店,右侧有 q 家店。

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

所以,我们应该将店设在最中间,即中位数。具体地,如果 n 是奇数,那货仓就建在 An+12 处;如果 n 是偶数,那货仓就可以建在 [An2,An+12] 上的任意位置。

#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. 七夕祭

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

接着考虑对于数组 a1,a2,...,an 的最小交换次数。

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

由于最后 a 中的每个数都要变为 a 的平均值 x¯,所以我们记 ci=aix¯,然后我们用一个前缀和数组 S,记录 c 的前缀和。

那么 i=1n|Si| 即为答案。

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

A:1 3 2 7 7

C:312 3 3

S:3463 0

发现了吗?S1 记录了 C1 应该被 C23,但是 C2 本身就是 1,又要考虑给 C1 的问题,所以 C2 总共需要 C3 给它 4C3 同理,需要 C46。到了 C4,它本身拥有 3,所以还需要 C5 给它 3

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

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

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

我们假设断点为 k,那么原前缀和序列的顺序就成了 Sk+1,...,Sn,S1,...,Sk,我们考虑修改过后的前缀和数组,即为 G

Gk+1=Sk+1Sk

Gk+2=Sk+2Sk

...

Gn=SnSk

G1=S1+SnSk

G2=S2+SnSk

...

Gk=Sk+SnSk

由于 Sn0,所以 Gi=SiSk

所以我们要找出使得 i=1n|SiSk| 最小的 k 即可。这很明显是货仓选址问题,所以 k=(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] 的形式,则此时左右交换不会影响此序列。上下交换,比如交换 47,则会增加两个逆序对。我们可以拓展到 n×n 的矩阵中,即上下交换会增加 n1 个逆序对。

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

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

#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 是从小到大排序的,那么校验值就应该是 (AnA1)2+(An1A2)2+...

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

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

分析下时间复杂度,设每个答案的长度为 len1,len2,...,lenk,那么对于每个 leni,需要倍增 O(logn)

那么总共的答案就是 O(logn×len1loglen1+logn×len2loglen2+...+logn×lenkloglenk)=O(logn(len1loglen1)+...+lenkloglenk)=O(n2logn)

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

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

这种思路下只需要将每个区间处理一次,时间复杂度就是 O(len1loglen1+...+lenkloglenk)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=i=2nmax(primesiprimesi1,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 最大的防晒霜。

证明:

首先,对于每一罐防晒霜 SPFi,因为我们已经按照 minSPF 进行了排序,所以如果
防晒霜 SPFx,大于 minSPFi,则它一定大于 minSPFi+1 ~ minSPFn
那么对于一头奶牛 i 与防晒霜 xy (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=yD2x2,r=y+D2x2

于是问题就转化成了,给定 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×b 从小到大排序,然后循环找最大值。

证明:

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

  1. 排序前,即 ai×bi>ai+1×bi+1

    此时大臣 i 的奖赏是:j=0i1ajbi

    此时大臣 i+1 的奖赏是:j=0iajbi+1

  2. 排序后,即 ai+1×bi+1>ai×bi

    此时大臣 i 的奖赏是:(j=0i1aj)×ai+1bi

    此时大臣 i+1 的奖赏是:j=0i1ajbi+1

把每个式子都提取公因式 j=0i1aj,再乘上 bi×bi+1
转为求:max(bi+1,ai×bi)max(bi,ai+1×bi+1) 的最大值

通过推导可以得出,当 ai×bi<ai+1×bi+1 时,交换前更优;
反之,交换后更优。所以无论如何我们都应该按照 ai×bi 排序。

要写高精度。

#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,那么 ab 取的顺序一定是相邻的,所以我们可以把它们当作一个点来做。

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

  1. a,b,再 cS1=a+2b+3c
  2. c,再 a,bS2=c+2a+3b

我们考虑作差:S1S2=a+2b+3cc2a3b=2c(a+b)=2(ca+b2)

所以当 c>a+b2 时 我们应该先染式子右边,否则先染式子左边。

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

假设有两个序列 a1,a2,...anb1,b2,...,bm

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

  1. S1=i=1nai×i+i=n+1n+mbi×i
  2. S2=i=1mbi×i+i=m+1n+mai×i

作差:S1S2=ni=1mbimi=1nai

所以当 S1<S2 时,i=1mbim<i=1nain
,反之亦然。

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

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

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

首先最开始先加上每个点的点权。对于当前要合并的 ab,如果把 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,然后二进制枚举 02161,然后判断该操作是否合法。

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

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

时间复杂度为:O(216×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 层一共有 3i1 行,并且可以用五个上一层的图形进行拼接而成。

我们找到每个图形的左上角坐标,分别为 (1,1),(1,3i2×2+1),(3i2+1,3i2+1),(3i2×2+1,1),(3i2×2+1,3i2×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,所以小长方形的长为 23r,宽为 12r,那么在长方形内部的最长距离也就是对角线长度为 (23r)2+(12r)2=56r<r=ans,所以不可能有 7 个点。

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

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

#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].sx 说明两个区间存在交集,那么交集应该为 [p[i].s,min(p[i].e,x)]

根据小学奥数,交集内部的数量就应该为(末项-首项)/ 公差 + 1,即 (min(p[i].e,x)p[i].s)÷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 函数。

由于我们已经预处理好了前缀和,我们就可以枚举正方形的左上角和右下角,然后通过前缀和来计算答案。一个细节是,因为四叶草的坐标是格子的左下角,所以我们在计算是要用 x1x0+1y1y0+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. 士兵

很容易发现我们可以把 xy 分开来做。

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

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

比如最左边的点走到 p1=k,则第 i 个人走到 pi=k+(i1)

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

所以问题又变为了货仓选址,我们只需要给每个 xii 即可。

#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(n4)

我们考虑优化,只枚举矩阵的起始行和结尾行,即 x1x2,然后用类似最长连续子段和的思路从 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. 任务

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

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

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

#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 @   比翼の鼠  阅读(7)  评论(0编辑  收藏  举报
//雪花飘落效果
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示