垃圾ACMer的暑假训练220710
蓝桥杯算法训练
2. 二分与前缀和
①二分前一个区间 A A A的右端点:将 [ l , r ] [l,r] [l,r]分为 [ l , m i d − 1 ] [l,mid-1] [l,mid−1]和 [ m i d , r ] [mid,r] [mid,r].若 m i d mid mid满足 A A A的性质,则 a n s ans ans在 [ m i d , r ] [mid,r] [mid,r]中,否则 a n s ans ans在 [ l , m i d − 1 ] [l,mid-1] [l,mid−1]中.
while(l < r) {
int mid = l + r + 1 >> 1; // +1避免l=r-1时死循环
if(mid满足A的性质) l = mid; // 判断标志
else r = mid - 1;
}
②二分后一个区间 B B B的左端点:将 [ l , r ] [l,r] [l,r]分为 [ l , m i d ] [l,mid] [l,mid]和 [ m i d + 1 , r ] [mid+1,r] [mid+1,r].若 m i d mid mid满足 B B B的性质,则 a n s ans ans在 [ l , m ] [l,m] [l,m]中,否则 a n s ans ans在 [ m i d + 1 , r ] [mid+1,r] [mid+1,r].
while(l < r) {
int mid = l + r >> 1; // l=r-1时不会死循环
if(mid满足B的性质) r = mid; // 判断标志
else l = mid + 1;
}
2.1 机器人跳跃 ( 3 s ) (3\ \mathrm{s}) (3 s)
题意
有编号 0 ∼ n ( 1 ≤ n ≤ 1 e 5 ) 0\sim n\ \ (1\leq n\leq 1\mathrm{e}5) 0∼n (1≤n≤1e5)的 ( n + 1 ) (n+1) (n+1)个建筑从左到右排列.编号为 0 0 0的建筑高度为 0 0 0,编号为 i ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i (1≤i≤n)的建筑高度为 h i ( 1 ≤ h i ≤ 1 e 5 ) h_i\ \ (1\leq h_i\leq 1\mathrm{e}5) hi (1≤hi≤1e5).初始时机器人在编号为 0 0 0的建筑处,每一步它将跳到右边的下一个建筑处.设机器人在第 k k k个建筑处,且它当前的能量为 E E E,下一步它将跳到第 ( k + 1 ) (k+1) (k+1)个建筑.若 h k + 1 > E h_{k+1}>E hk+1>E,则机器人失去 h k + 1 − E h_{k+1}-E hk+1−E的能量,否则它得到 E − h k + 1 E-h_{k+1} E−hk+1的能量.游戏目标是到达第 n n n个建筑,途中能量不能为负值.求机器人至少以多少能量开始游戏才能保证成功完成游戏.
第一行输入一个 n n n.第二行输入 n n n个整数 h 1 , ⋯ , h n h_1,\cdots,h_n h1,⋯,hn.
输出机器人所需的最小能量,答案上取整.
思路
① h k + 1 > E h_{k+1}>E hk+1>E时, E − ( h k + 1 − E ) = 2 E − h k + 1 E-(h_{k+1}-E)=2E-h_{k+1} E−(hk+1−E)=2E−hk+1.
② h k + 1 ≤ E h_{k+1}\leq E hk+1≤E时, E + ( E − h k + 1 ) = 2 E − h k + 1 E+(E-h_{k+1})=2E-h_{k+1} E+(E−hk+1)=2E−hk+1.
故两种情况的能量更新都是 2 E − h k + 1 2E-h_{k+1} 2E−hk+1,
初始能量具有单调性,即若初始能量为 E 0 E_0 E0时满足条件,则 ∀ E 0 ′ ≥ E \forall E_0'\geq E ∀E0′≥E也满足条件.
[证] 设初始能量为 E 0 E_0 E0时,机器人到达第 1 ∼ n 1\sim n 1∼n个建筑处的能量依次为 E 1 , ⋯ , E n ( E i ≥ 0 , 1 ≤ i ≤ n ) E_1,\cdots, E_n\ \ (E_i\geq 0,1\leq i\leq n) E1,⋯,En (Ei≥0,1≤i≤n).
初始能量为 E 0 ′ E_0' E0′时,注意到 E 1 ′ = 2 E 0 ′ − h 1 ≥ 2 E 0 − h 1 = E 1 E_1'=2E_0'-h_1\geq 2E_0-h_1=E_1 E1′=2E0′−h1≥2E0−h1=E1,数归即证.
check()函数可递推一遍看中途会不会出现负值,但这样每次能量乘 2 2 2会爆long long.设建筑物高度的最大值为 m a x h maxh maxh.若 E ≥ m a x h E\geq maxh E≥maxh,则 2 E − h i ≥ E + E − h i ≥ E + ( m a x h − h i ) ≥ E 2E-h_i\geq E+E-h_i\geq E+(maxh-h_i)\geq E 2E−hi≥E+E−hi≥E+(maxh−hi)≥E,即能量单调增.故只需中途能量 E ≥ m a x h E\geq maxh E≥maxh即可返回true.
代码
const int MAXN = 1e5 + 5;
int n;
int h[MAXN];
int maxh = -INF;
bool check(int e) {
for (int i = 1; i <= n; i++) {
e = e * 2 - h[i];
if (e >= maxh) return true;
if (e < 0) return false;
}
return true;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> h[i];
maxh = max(maxh, h[i]);
}
int l = 0, r = 1e5;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << l;
}
2.2 四平方和
题意
输入一个正整数 n ( 0 < n < 5 e 6 ) n\ \ (0<n<5\mathrm{e}6) n (0<n<5e6),求 0 ≤ a ≤ b ≤ c ≤ d s . t . n = a 2 + b 2 + c 2 + d 2 0\leq a\leq b\leq c\leq d\ s.t.\ n=a^2+b^2+c^2+d^2 0≤a≤b≤c≤d s.t. n=a2+b2+c2+d2,对所有的 ( a , b , c , d ) (a,b,c,d) (a,b,c,d)升序排列,输出字典序最小的表示法.
思路
注意到 a , b , c , d ≤ n ≤ 2232 a,b,c,d\leq \sqrt{n}\leq 2232 a,b,c,d≤n≤2232,则最多只能枚举两个数.
可先枚举较大的 c c c和 d d d,记录下所有 c 2 + d 2 c^2+d^2 c2+d2可能的取值.再枚举较小的 a a a和 b b b,计算 t = n − a 2 − b 2 t=n-a^2-b^2 t=n−a2−b2,判断 t t t是否在 c 2 + d 2 c^2+d^2 c2+d2中出现过,可用哈希 O ( 1 ) O(1) O(1)或二分 O ( log n ) O(\log n) O(logn)实现,二分的时间复杂度 5 e 6 × lg ( 5 e 6 ) ≈ 1 e 8 5\mathrm{e}6\times \lg(5\mathrm{e}6)\approx 1\mathrm{e}8 5e6×lg(5e6)≈1e8,但枚举时 a ≤ b ≤ c ≤ d a\leq b\leq c\leq d a≤b≤c≤d,故复杂度约要除以 2 2 2,可过.
注意到 a a a从 0 0 0开始枚举, b b b从 a a a开始枚举,若找到解,则 ( a , b ) (a,b) (a,b)字典序最小.对 { c 2 + d 2 , c , d } \{c^2+d^2,c,d\} {c2+d2,c,d}升序排列即可保证解的 ( c , d ) (c,d) (c,d)字典序最小.
总时间复杂度:①暴力枚举 O ( n 3 ) O(n^3) O(n3);②哈希 O ( n 2 ) O(n^2) O(n2);③二分 O ( n 2 log n ) O(n^2\log n) O(n2logn).实际运行中二分最快,因为哈希表常数大.
代码I:暴力枚举(TLE)
int n;
int main() {
cin >> n;
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
for (int c = b; a * a + b * b + c * c <= n; c++) {
int t = n - a * a - b * b - c * c;
int d = sqrt(t);
if (d * d == t)
return cout << a << ' ' << b << ' ' << c << ' ' << d << endl, 0;
}
}
}
}
代码II:二分
const int MAXN = 2.5e6 + 5; // 5e6 / 2
int n;
struct Sum {
int s, c, d; // s=c^2+d^2
bool operator<(const Sum& p)const {
if (s != p.s) return s < p.s;
else if (c != p.c) return c < p.c;
else return d < p.d;
}
}sum[MAXN];
int main() {
cin >> n;
// 预处理出所有c^2+d^2
int idx = 0;
for (int c = 0; c * c <= n; c++)
for (int d = c; c * c + d * d <= n; d++)
sum[idx++] = { c * c + d * d,c,d };
sort(sum, sum + idx);
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b;
// 二分是否存在c^2+d^2=t
int l = 0, r = idx - 1;
while (l < r) {
int mid = l + r >> 1;
if (sum[mid].s >= t) r = mid;
else l = mid + 1;
}
if (sum[l].s == t) // 判断是否存在
return cout << a << ' ' << b << ' ' << sum[l].c << ' ' << sum[l].d << endl, 0;
}
}
}
代码III:哈希表(TLE)
int n;
umap<int, pii> s; // s[c^2+d^2]={c,d}
int main() {
cin >> n;
// 预处理出所有c^2+d^2
int idx = 0;
for (int c = 0; c * c <= n; c++) {
for (int d = c; c * c + d * d <= n; d++) {
int t = c * c + d * d;
if (!s.count(t)) s[t] = { c,d }; // 只记录第一个出现的c^2+d^2
}
}
for (int a = 0; a * a <= n; a++) {
for (int b = a; a * a + b * b <= n; b++) {
int t = n - a * a - b * b;
if (s.count(t))
return cout << a << ' ' << b << ' ' << s[t].first << ' ' << s[t].second << endl, 0;
}
}
}
2.3 分巧克力
题意
有 k ( 1 ≤ k ≤ 1 e 5 ) k\ \ (1\leq k\leq 1\mathrm{e}5) k (1≤k≤1e5)个小朋友和有 n ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n (1≤n≤1e5)块巧克力,其中第 i ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i (1≤i≤n)块是 h i × w i ( 1 ≤ h i ≤ w i ≤ 1 e 5 ) h_i\times w_i\ \ (1\leq h_i\leq w_i\leq 1\mathrm{e}5) hi×wi (1≤hi≤wi≤1e5)的长方形.现需从 n n n块巧克力中切出 k k k块分给小朋友,要求巧克力形状是边长为整数的大小相同的正方形.求每个小朋友能得到的最大边长.数据保证每个小朋友至少能获得 1 × 1 1\times 1 1×1的正方形.
思路
边长为 w i × h i w_i\times h_i wi×hi能切出边长为 x x x的正方形 ⌊ w i x ⌋ ⌊ h i x ⌋ \left\lfloor\dfrac{w_i}{x}\right\rfloor\left\lfloor\dfrac{h_i}{x}\right\rfloor ⌊xwi⌋⌊xhi⌋个.显然边长越大,能切出来的块数越小,即具有单调性.显然可二分出块数 ≥ k \geq k ≥k的最大边长.
check()函数需扫一遍 n n n块巧克力.总时间复杂度 O ( n log max { h i , w i } ) O(n\log\max\{h_i,w_i\}) O(nlogmax{hi,wi}).
代码
const int MAXN = 1e5 + 5;
int n, k; // 巧克力数、人数
int h[MAXN], w[MAXN];
bool check(int m) {
int res = 0;
for (int i = 0; i < n; i++) {
res += (h[i] / m) * (w[i] / m);
if (res >= k)return true;
}
return false;
}
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> h[i] >> w[i];
int l = 1, r = 1e5;
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
cout << l;
}
2.4 激光炸弹 ( 168 M B 168\ \mathrm{MB} 168 MB)
题意
地图上有 n ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n (1≤n≤1e4)个目标,其中第 i ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i (1≤i≤n)个目标的坐标为 ( x i , y i ) ( 0 ≤ x i , y i ≤ 5000 ) (x_i,y_i)\ \ (0\leq x_i,y_i\leq 5000) (xi,yi) (0≤xi,yi≤5000),价值为 w i ( 0 ≤ w i ≤ 1000 ) w_i\ \ (0\leq w_i\leq 1000) wi (0≤wi≤1000),不同目标可能在同一位置.激光炸弹可摧毁 r × r ( 1 ≤ r ≤ 1 e 9 ) r\times r\ \ (1\leq r\leq 1\mathrm{e}9) r×r (1≤r≤1e9)的正方形的所有目标,其中炸弹只能放在整点上,且爆炸范围的正方形的边平行于 x x x、 y y y轴.求一颗炸弹能炸掉的目标的最大总价值.
思路
r r r最多取 5001 5001 5001即可覆盖整个地图.若地图不够 r × r r\times r r×r的正方形大,则可能会出现边界问题,故让地图的行数和列数至少为 r r r.
暴力枚举各以 ( i , j ) (i,j) (i,j)为右下角的、边长为 r r r的正方形内的和.
若开一个 5000 × 5000 5000\times 5000 5000×5000的原数组和一个 5000 × 5000 5000\times 5000 5000×5000的前缀和数组,则需 2 × 500 0 2 × 4 1024 × 1024 ≈ 200 M B \dfrac{2\times 5000^2\times 4}{1024\times 1024}\approx 200\ \mathrm{MB} 1024×10242×50002×4≈200 MB,会MLE,故只能开一个前缀和数组.
代码
const int MAXN = 5005;
int n, m; // 行数、列数
int pre[MAXN][MAXN];
int main() {
int cnt, r; cin >> cnt >> r;
r = min(5001, r);
n = m = r; // 防边界问题
while (cnt--) {
int x, y, w; cin >> x >> y >> w;
x++, y++; // 下标从1开始
n = max(n, x), m = max(m, y);
pre[x][y] += w;
}
for (int i = 1; i <= n; i++) // 预处理前缀和
for (int j = 1; j <= m; j++)
pre[i][j] += pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1];
int ans = 0;
for (int i = r; i <= n; i++) // 枚举以(i,j)为右下角的边长为r的正方形
for (int j = r; j <= m; j++)
ans = max(ans, pre[i][j] - pre[i - r][j] - pre[i][j - r] + pre[i - r][j - r]);
cout << ans;
}
2.5 k倍区间
题意
对长度为 n ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n (1≤n≤1e5)的数列 a 1 , ⋯ , a n ( 1 ≤ a i ≤ 1 e 5 ) a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}5) a1,⋯,an (1≤ai≤1e5),若其中一段连续的子序列 a i , ⋯ , a j a_i,\cdots,a_j ai,⋯,aj之和是 k ( 1 ≤ k ≤ 1 e 5 ) k\ \ (1\leq k\leq 1\mathrm{e}5) k (1≤k≤1e5)的倍数,则称区间 [ i , j ] [i,j] [i,j]为 k k k倍区间.求序列中 k k k倍区间的个数.
思路
枚举每个区间 [ l , r ] [l,r] [l,r],用前缀和求区间内的和,总时间复杂度为 O ( n 2 ) O(n^2) O(n2).
考虑优化.枚举每个区间 [ l , r ] [l,r] [l,r],其中的数的和为 p r e [ r ] − p r e [ l − 1 ] pre[r]-pre[l-1] pre[r]−pre[l−1],即在 0 ∼ r 0\sim r 0∼r的范围求有多少个 l s . t . ( p r e [ r ] − p r e [ l − 1 ] ) % k = 0 l\ s.t.\ (pre[r]-pre[l-1])\%k=0 l s.t. (pre[r]−pre[l−1])%k=0,亦即在 0 ∼ ( r − 1 ) 0\sim (r-1) 0∼(r−1)的范围求有多少个 l s . t . ( p r e [ r ] − p r e [ l ] ) % k = 0 l\ s.t.\ (pre[r]-pre[l])\%k=0 l s.t. (pre[r]−pre[l])%k=0,即有多少个 p r e [ l ] pre[l] pre[l]与 p r e [ r ] pre[r] pre[r]模 k k k的余数相等.用数组 c n t [ i ] cnt[i] cnt[i]表示余数为 i i i的数的个数.总时间复杂度 O ( n ) O(n) O(n).
代码
const int MAXN = 1e5 + 5;
int n, k;
ll pre[MAXN];
int cnt[MAXN]; // cnt[i]表示余数为i的数的个数
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> pre[i];
pre[i] += pre[i - 1];
}
ll ans = 0;
cnt[0] = 1; // pre[0]=0,模k余0
for (int i = 1; i <= n; i++) {
ans += cnt[pre[i] % k];
cnt[pre[i] % k]++;
}
cout << ans;
}