目录
写在前面
作为一名ACM新人,我最近正在研究根号分治算法。与其说是算法,不如说是一种思想,像dp一样。根号分治的思想就是把数据范围分成大于根号的部分和小于根号的部分,在这两个部分中分别运用不同的算法去解决相应的问题。
洛谷 P3396 哈希冲突
题目大意
给你一个长度为 n n n的正整数序列和 m m m次操作,对于每次操作,按照如下格式输入,你需要回答如下问题:
- A A A x x x y y y:统计下标模 x x x余 y y y的元素总和,即 a y + a x + y + a 2 x + y + ⋯ a_y+a_{x+y}+a_{2x+y}+\cdots ay+ax+y+a2x+y+⋯;
- C C C x x x y y y:令 a x = y a_x=y ax=y。
数据范围: 1 ≤ n , m ≤ 1.5 × 1 0 5 1\le n,m \le1.5\times 10^5 1≤n,m≤1.5×105, 1 ≤ a i ≤ 1 0 3 1 \le a_i \le 10^3 1≤ai≤103,且对于操作 A A A,保证 1 ≤ y < x ≤ n 1\le y < x \le n 1≤y<x≤n。
样例参见题目链接。
解题思路
这道题是根号分治的经典例题。先考虑操作A,我们观察可以发现,对于每一个 x x x,直接计算一遍答案的时间复杂度为 O ( n x ) O(\frac{n}{x}) O(xn)。当 x x x越大时,需要计算的次数就越少,同时,对于每一个 x x x,我们都可以预处理出结果,预处理所需的时间复杂度为 O ( n ) O(n) O(n)。因此,我们考虑将以上两种方法结合起来,并且取一个平衡点: n \sqrt{n} n。
当 x ≤ n x \le \sqrt{n} x≤n时,采用预处理的方法,将所有答案预处理出来,时间复杂度为 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23),这样询问的复杂度就为 O ( 1 ) O(1) O(1)。而当 x ≥ n x \ge \sqrt{n} x≥n时,采用直接计算的方法,计算一次的最坏时间复杂度为 O ( n ) O(\sqrt{n}) O(n)。至于 x = n x=\sqrt{n} x=n的时候,可以随意归为任何一类,无需特殊考虑。
综上,操作A的时间复杂度为 O ( n ( n + m ) ) O(\sqrt{n}(n+m)) O(n(n+m)),可以通过此题。
而修改操作非常容易:对于每一次修改,我们需要先计算出修改前和修改后的差值,用于更新前面预处理的部分,然后再将原数组中的数更改,时间复杂度为 O ( n ) O(\sqrt{n}) O(n)。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 2e5+7;
const int M = 505; // 用来开根号分治的数组大小,一般略大于sqrt(n)
int arr[N]; // 用于储存原数组
int f[M][M]; // 用于存储根号分治预处理的数组
void solve() {
int n, m;
cin >> n >> m;
fori(1, n) cin >> arr[i];
const int G = sqrt(n); // 根号分治的根号大小
fori(1, G) forj(1, n) f[i][j%i] += arr[j]; // 预处理根号分治的数组
while (m--) {
string op;
int x, y;
cin >> op;
cin >> x >> y;
if (op[0] == 'A') {
if (x<=G) cout << f[x][y] << endl; // 小于根号的部分,直接输出预处理的值
else { // 大于根号分治的部分,直接暴力求解
int sum = 0;
for (int i=y;i<=n;i+=x) sum += arr[i];
cout << sum << endl;
}
}
else {
int ch = y-arr[x]; // 记录修改的大小
arr[x] = y;
fori(1, G) f[i][x%i] += ch; // 在根号分治的预处理结果中修改
}
}
}
signed main() {
IOS;
int t = 1;
while (t--) {
solve();
}
return 0;
}
Codeforces 1997E Level Up
题目大意
你在进行一场打怪游戏,你的初始等级为1,你将面对 n n n个怪物,每个怪物都有一个等级 a i a_i ai,每当你打死了 k k k个怪物,你的等级就会 + 1 +1 +1,但对于每一个怪物,如果你当前的等级严格大于该怪物的等级,那么这只怪物会逃跑,你将无法获取到打死这个怪物的经验值(即不能将这个怪物算作你打死的怪物)。也就是说,只有等级大于等于你目前的等级的怪物会被计算在内。
有 q q q个询问,每次询问会给出 i x i \ x i x,代表当 k = x k=x k=x时,你是否会打第 i i i个怪物。如果会打,输出YES;如果怪物逃跑了,输出NO。
数据范围: 1 ≤ n , q ≤ 2 × 1 0 5 1 \le n,q \le 2\times 10^5 1≤n,q≤2×105, 1 ≤ a i ≤ 2 × 1 0 5 1 \le a_i \le 2\times 10^5 1≤ai≤2×105, 1 ≤ i , x ≤ n 1 \le i,x \le n 1≤i,x≤n。
解题思路
这道题我在赛时没有来得及写完,因为前面的BC想复杂了想了好久,D还WA了一发,这一场直接怒掉了40多分(还是我太菜了)。后面才把这道题补了,其实也是非常经典的根号分治思想:首先离线,考虑 x ≤ n x\le \sqrt{n} x≤n时,将答案直接预计算出来,复杂度为 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23)。
当 x ≥ n x\ge \sqrt{n} x≥n时,会稍微复杂一些,此时发现可能升级到的最大等级仅为 ⌊ n x ⌋ + 1 ≤ n + 1 \lfloor \frac{n}{x} \rfloor+1 \le \sqrt{n}+1 ⌊xn⌋+1≤n+1,因此考虑预处理出所有大于等于 n + 1 \sqrt{n}+1 n+1的怪物数量前缀和,记录在一个二维数组当中,然后对于每个 x x x利用二分查找答案。预处理时间复杂度为 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23),查找答案的时间复杂度为 O ( n 3 2 log n ) O(n^{\frac{3}{2}} \log n) O(n23logn)。
总时间复杂度为 O ( n 3 2 log n ) O(n^{\frac{3}{2}} \log n) O(n23logn),空间复杂度为 O ( n 3 2 ) O(n^{\frac{3}{2}}) O(n23)。
可能还有更快速且简便的做法,各位可以去参考其他dalao的解题思路。
AC代码
我的离线存储答案机制写的太垃圾了,差点导致MLE(最后1500ms,490000KB也是卡了个极限过去了)
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back
#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)
#define debug(x) cout << #x << " = " << x << endl
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
const int N = 2e5+7;
const int M = 605;
int arr[N];
int f[M][N]; // 分块后
vector<pii> que;
map<int,bool> ans[N];
void solve() {
int n, m;
cin >> n >> m;
fori(1, n) cin >> arr[i];
fori(1, m) {
int a,b;
cin >> a >> b;
que.push_back({a, b});
ans[b][a];
}
int G = sqrt(n); // 分块大小
forj(1, G+1) {
int lv = 1;
int cnt = 0;
fori(1, n) { // 预处理出小于sqrt(n)的答案
if (arr[i]<lv) {
if (ans[j].find(i) != ans[j].end()) ans[j][i] = false;
}
else {
cnt++;
if (ans[j].find(i) != ans[j].end()) ans[j][i] = true;
if (cnt>=j) {
cnt = 0;
lv++;
}
}
} // 预处理出级别<=sqrt(n)的怪物数量
fori(1, n) f[j][i] = f[j][i-1]+(arr[i]>=j);
}
fork(G+1, n) {
vector<int> pt;
int lv = 1;
int now = 0;
while (1) {
int nxt = lower_bound(f[lv]+1, f[lv]+n+1, f[lv][now]+k)-f[lv]; // 下一个升级点的位置
if (nxt>n) break;
pt.push_back(nxt);
now = nxt;
lv++;
}
for (auto pi: ans[k]) {
int lv = lower_bound(pt.begin(), pt.end(), pi.first)-pt.begin()+1; // 处理答案
if (arr[pi.first]<lv) ans[k][pi.first] = false;
else ans[k][pi.first] = true;
}
}
// ans
fori(0, m-1) {
int a = que[i].first, b = que[i].second;
if (ans[b][a]) YES;
else NO;
}
}
signed main() {
IOS;
int t = 1;
while (t--) {
solve();
}
return 0;
}