前言
感谢 mkr 同学积极和我协商沟通,使我更了解大家的情况。
考虑到很多高级算法难度过大,并且以后大家也会逐渐接触,所以这些东西我就不写了。
本篇文章从一个特殊的角度切入,着重介绍一类着有相同思想的题目,希望大家感受到这股劲。
虽然我读的博客也不多,但是我没有见过和本篇文章相似的博客(骄傲)。
文章配套题单也迁移到了团队题单中。
正文
你有没有见过这样一类题目,你很快想到一个复杂度比较劣的做法,却怎么也想不出正解。
大战两小时拼尽全力无法战胜后,你打开了题解:
按题意模拟即可,复杂度为 \(O(n \sqrt{n})\)。
或者是:
不难发现直接做就是对的。
甚至是:
用脚维护,不需要脑子,我写了。
你发现最开始你想的做法是正确的,只不过需要一点小小的改变。可能是改变枚举顺序,可能是加上一点小小的剪枝,可能是记忆化。甚至有可能你尝试实现了你的想法,以为会 T 飞,但是实际上跑的飞快,直接草过。
此时有一个 OIer 轻轻地碎掉了。
在你的意识逐渐模糊之前,你的脑子里只剩下一句话:
这类题到底应该怎么思考?
看小说看多了导致的。
这类题一般被称为暴力题。
对暴力题做一些更准确的描述:暴力题正解的复杂度看起来很高,但是经过精细分析与惊喜实现后,复杂度很优秀,是可以通过的。
-
如果你见过暴力题,并且苦于这类题应该怎样入手思考,那么恭喜你,这篇文章正是你想要的;
-
如果你没有见过暴力题,那么也恭喜你,今天你能看到六道暴力题,感受做(chi)题(shi)的快乐;
-
如果你觉得题目有点简单,那么也恭喜你,有了一个装逼的机会。请私信嘲讽我,我会添加一些更加困难的题目,并膜拜您因为您太强了。
反正恭喜你.jpg
在我学 OI 的经历中,暴力题一般和思维题放在一起讲,暴力题的做法实际上是思维难度高,实现难度低的做法。
但是我觉得有必要把这类题单独摘出来说,让大家充分感受暴力美学。
今天我找来了六道有代表性的暴力题,题目难度按我主观感受难度排序。
因为所有题的思维难度和实现难度都不高,所以建议有精力的话都要好好思考理解。
和上一篇周报不一样,因为这类题的思考方式都比较相似,所以这次不再设置提示。
同样地,在看做法之前,应该先独立思考,如果有很好的想法的话可以写完直接交,万一过了呢。
让我们开始吧!
例题一
[JRKSJ R6] Eltaw
有无音游老哥给大家弹一首 Eltaw。
按题意模拟即可。
具体地,对于每次询问 \(l,r\),如果 \(l,r\) 在之前询问过,那么直接回答询问。否则,暴力枚举 \(k\) 个序列,用前缀和求出区间和,取最大值。
可以证明复杂度不超过 \(O(\max(q,nk) \sqrt{nk})\),可以通过本题。
考虑证明:
-
当 \(n \le \sqrt{nk}\),即 \(n \le k\) 时,因为至多有 \(O(n^2)\) 种不同的 \((l,r)\),对于每一组 \((l,r)\),都需要 \(O(k)\) 的复杂度来求出答案,总体复杂度为 \(O(n^2k)\),即 \(O(nk \sqrt{nk})\)。
-
当 \(n > \sqrt{nk}\),即 \(n > k\) 时,因为至多有 \(q\) 次询问,每次询问复杂度为 \(O(k)\),总体复杂度为 \(O(qk)\),即 \(O(q \sqrt{nk})\)。
综上,复杂度不超过 \(O(\max(q,nk) \sqrt{nk})\)。
#include <bits/stdc++.h>
using namespace std;
int n, k, q;
const int N = 5e5 + 5;
int *a[N];
long long *s[N];
long long ans[705][705];
inline int read() {int x = 0;char ch = getchar();while (!isdigit(ch)) ch = getchar();while (isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();return x;
}
int main() {ios::sync_with_stdio(false);cout.tie(NULL);n = read(), k = read(), q = read();for (int i = 1; i <= k; i++) {a[i] = (int*)calloc(n + 1, sizeof(int));for (int j = 1; j <= n; j++)a[i][j] = read();}for (int i = 1; i <= k; i++) {s[i] = (long long*)calloc(n + 1, sizeof(long long));for (int j = 1; j <= n; j++) s[i][j] = s[i][j - 1] + a[i][j];}if (n <= sqrt(n * k)) {memset(ans, ~0x7f, sizeof ans);for (int i = 1; i <= k; i++) {for (int j = 1; j <= n; j++)for (int l = j; l <= n; l++) {ans[j][l] = max(ans[j][l], s[i][l] - s[i][j - 1]);}}for (int i = 1; i <= q; i++) {int l = read(), r = read();cout << ans[l][r] << '\n';}} else {for (int i = 1; i <= q; i++) {int l = read(), r = read();long long res = -1;for (int j = 1; j <= k; j++)res = max(res, s[j][r] - s[j][l - 1]);cout << res << '\n';}}return 0;
}
例题二
模
如果你想直接上线段树维护取模那么建议重学线段树。
推销我在 S2OJ 上写的题解:here.
难受的是,取模运算没有交换律也没有结合律,也就意味着用什么数据结构维护区间取模都是妄想。
但是,取模有一个很有意思的性质:在经过取模运算之后,被模数只会越来越小,并且如果被模数改变,那么被模数至少减少至原来的一半。
此时的你:草这不是做完了吗。
具体地,每次在区间 \([l,r]\) 中寻找最小的 \(i\),使得 \(x \ge a_i\),令 \(x \to x \bmod g_i\),直到区间里的都 \(>x\) 为止。
每次寻找 \(i\) 可以用 ST 表加二分实现,也可以用线段树上二分实现,甚至可以用文艺平衡树做,复杂度都是 \(O(\log n)\),相信大家都会。
根据上面的结论,每次取模都会使 %x% 减半,那么取模的总次数不会超过 \(\log x\)。
所以,总体复杂度为 \(O(n \log n \log x)\)。
我写的是线段树上二分,但是考虑到很多人不会,所以我把别人的 ST 表加二分贺过来了。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+100;inline int read()
{int x = 0, f = 1; char ch = getchar();while(ch<'0'||ch>'9'){if(ch == '-') f = -1;ch = getchar();}while(ch>='0'&&ch<='9'){x = x*10+ch-48; ch = getchar();}return x*f;
}
int lg[N];int a[N], n, m;
int st[30][N];
void prelg(){for(int i = 2; i<=n; i++) lg[i] = lg[i>>1]+1;
}
void add(int pos)
{st[0][pos] = pos;for(int i = 1; i<=lg[pos]; i++){if(a[st[i-1][pos]]>a[st[i-1][pos-(1<<(i-1))]]){st[i][pos] = st[i-1][pos-(1<<(i-1))];}else{st[i][pos] = st[i-1][pos];}}//这里使用st表存最小值位置,写的真滴丑qwq//貌似存值也可以,本蒟蒻想多了xwx
} int query(int l, int r)
{int lth = r-l+1;if(a[st[lg[lth]][l+(1<<lg[lth])-1]]>a[st[lg[lth]][r]]) return st[lg[lth]][r];else return st[lg[lth]][l+(1<<lg[lth])-1];
}
int find(int lq,int rq, int tar)
{int ans = rq+1, ln = lq, rn = rq;//未找到则返回rq+1while(ln<=rn){int mid = (ln+rn)>>1;if(a[query(ln, mid)]<=tar){rn = mid-1;ans = mid;}else{ln = mid+1;}}return ans;
}
int main()
{n = read(), m = read();prelg();for(int i = 1; i<=n ;i++){a[i] = read();add(i); }while(m--){int x = read(), l = read(), r = read(); while(l<=r){l = find(l, r, x);if(l>r) break;else x%=a[l];}printf("%d\n", x);}return 0;
}