蓝书 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\) 后 \(x <= m\)
- 该位填 \(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_{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\) 即可。
#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\) 的奇偶性分两类讨论:
-
\(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})\)
-
\(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\) 可以为任意数。
接下来考虑操作,一共四种情况:
- 选择 \(b_1\) 和 \(b_{n+1}\)
- 选择 \(b_1\) 和 \(b_{j}(j \leq n)\)
- 选择 \(b_j(j > 1)\) 和 \(b_{n+1}\)
- 选择 \(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\) 家店。
- 如果 \(p < q\),那么 \(x\) 每右移一个单位长度,距离之和就会变小 \(q - p\)
- 如果 \(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\),然后进行如下操作:
- 定义变量 \(r = l, p = 1\)
- 循环判断 \([l, r + p)\) 的校验值是否小于等于 \(T\)。
- 如果是,
r += p, p *= 2
- 如果不是,
p /= 2
- 如果是,
- 当 \(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\) 天,分两种情况。
- 当前持有股票,则若下一天是涨,我们肯定会把股票留着,否则,我们应该直接卖出。
- 当前未持有股票,如果下一天涨,我们应该在今天买入,否则不应该买入。
所以可以得到 \(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)\) 来说,只有以下三种情况:
- \(x\) 能用,\(y\) 能用
- \(x\) 能用,\(y\) 不能用
- \(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\) 排序,对于每个区间:
- 如果区间内已经有了点,就跳过这个区间。
- 如果区间内没有点,就在区间的右端点找点。
正确性显然,不予证明。
#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\) 号大臣。
-
排序前,即 \(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}}\)
-
排序后,即 \(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\),那我们有两种染法:
- 先 \(a,b\),再 \(c\):\(S_1 = a + 2b + 3c\)
- 先 \(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\)。
与之相似我们也有两种染法:
- \(S_1 = \sum_{i = 1}^{n}a_i \times i + \sum_{i = n + 1}^{n + m}b_i \times i\)
- \(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;
}