HDU 6315 Naive Operations 离线 + 线段树
写在前面
因为今天师弟们训练打多校, 再次翻到了这道题, 想起去年多校比赛时想到了做法但是却没有敲出来, 十分遗憾, 但是赛后询问TMK他们的做法的时候, 感到十分惊讶, 因为我的做法是离线 + 两棵线段树计算贡献, 他们是维护最小值暴力更新, 惊叹他们做法的巧妙之余也对自己的算法有了怀疑, 在题解出来后更是觉得想要验证一下自己的想法, 于是在当天晚上敲了一发并AC, 证明了自己的想法是对:
不过跑起来比较慢, 跑了将近1s, 队友在看了题解之后照着题解思路敲了一发, 时间不到900ms, 感觉我的做法确实常数比较大。
然后想起自己这个不一样的做法, 就翻出来代码, 对着师弟吹了一顿牛逼, 然后想到我这个做法虽然暴力, 但是跟题解不一样, 想着会不会比较少人想到, 就萌生出了写一篇题解的想法, 然后随便百度一下, 确实没什么人写, 不过有份题解里提到了“两棵线段树”, 感觉应该还是有人写了, 不过想了想, 为了记录此刻的心情, 还是写下来吧。
以下是题目以及题解正文:
Naive Operations
Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 502768/502768 K (Java/Others)
Problem Description
In a galaxy far, far away, there are two integer sequence a and b of length n.
b is a static permutation of 1 to n. Initially a is filled with zeroes.
There are two kind of operations:
1. add l r: add one for al,al+1…ar
2. query l r: query ∑ri=l⌊ai/bi⌋
Input
There are multiple test cases, please read till the end of input file.
For each test case, in the first line, two integers n,q, representing the length of a,b and the number of queries.
In the second line, n integers separated by spaces, representing permutation b.
In the following q lines, each line is either in the form ‘add l r’ or ‘query l r’, representing an operation.
1≤n,q≤100000, 1≤l≤r≤n, there’re no more than 5 test cases.
Output
Output the answer for each ‘query’, each one line.
Sample Input
5 12
1 5 2 4 3
add 1 4
query 1 4
add 2 5
query 2 5
add 3 5
query 1 5
add 2 4
query 1 4
add 2 5
query 2 5
add 2 2
query 1 5
Sample Output
1
1
2
4
4
6
题意
给你一个数组a, 初始全为0, 再给你一个排列b, 现在有q次操作, 操作1是给出了,r,让a[l]~a[r]+1,操作二是给出l,r询问⌊ai/bi⌋,其中i∈[l,r]。
首先题目这个东西确实不好算, 主要因为有个下取整, 不能用线段树直接维护, 此时就想到按每个位置的贡献来计算, 那么就可以想到, 位置i被加了bi次之后才会使得有询问到这个位置的答案加一。 但是此时问题依然没有解决, 因为暴力的做的复杂度难以估计, 而且在线维护贡献十分难, 于是便想到了离线 + 打标记。
假设我们两个vector数组,记为TADD和TQUE, 且TADD[i]表示那些影响到了位置i的操作1的时间点,例如TADD[i] = { 1, 3, 5 }; 的话就表示第1, 3, 5个操作影响到了位置 i 。TQUE也同理表示询问到了位置i的询问。
那么有了这个我们怎么处理贡献呢, 首先对于位置i,TADD[i]每有b[i] 元素就会对达到这个数量开始以后的询问产生1点贡献, 比如说TADD[i] = { 1, 3, 5, 7, 9 }; TQUE[i] = { 2, 4, 6, 8 }且b[i] = 2, 那么在TQUE[i]大于3的询问就会答案+1,然后TQUE[i] 中大于7的询问的答案再次加一,如此,我们就能算出每个询问的答案。
那么现在的问题的就变成了如何维护TADD[i] 以及TQUE[i] 以及更新答案了。
TADD[i] 的维护其实十分容易, 把询问离线之后给每个位置维护一个addL跟addR的vector就知道每个位置有哪些区间的开头结尾在这个位置了, 那么计算每个位置的贡献的时候, 先把addL里的每个询问加入TADD, 处理完贡献之后把每个addR退出TADD就可以了。而这个操作可以维护一个值域线段树表示操作x是否对当前位置产生贡献来解决, 这样我们就可以用log的代价得到每个产生贡献的位置的操作时间点了。
类似的, TQUE[i] 需要用来类似区间更新的操作, 且每次更新都是1, 那么显然我们可以用线段树的区间更新(记得带lazy标记)来维护,线段树中表示的位置x就表示在时间点x的询问的答案。
维护TQUE这里有个细节需要讲解一下的就是, 每次我们更新答案的时候, 假设现在在t以后的询问答案加一, 那么我们直接更新t~q的所有询问的话, 可能会把不涉及位置i的询问也更新了, 但是事实上我们通过一个小小的讨论和操作就可以解决这个问题了。 假如我们现在更新把某个不涉及当前位置的询问x给更新了, 那么假如这个x的右端点我们已经处理过了, 那么它的答案我们已经得到过了, 我们此时给这个x增加多余的答案并不会影响最终答案, 因为最终答案我们可以保存在另外一个数组ans中, 更新后不再去动它, 自然就不会影响答案; 反过来, 如果这个x的左端点我们还没有遇到, 我们这样提前给他加答案肯定是不行的, 那么很简单, 当我们遇到了这个询问x的左端点的时候, 把那个位置清零, 也就是把之前不小心给他加上的额外的答案去掉就可以了。
至此, 离线 + 两棵线段树维护答案的思路就是这样了, 复杂度的计算也类同其他博客。 首先主要的耗费的时间就是, 我们会遍历每一个位置, 此时, 我们以b[i] 的步长遍历TADD[i],而TADD[i]的长度最坏情况为每个都是n, 那么总的复杂度就是n / b1 + n / b2 + … + n / bn = n * ( 1 + 1 / 2 + 1 / 3 + … + 1 / n ) ≈ n * logn, 同时, 以b[i] 的步长的时候进行遍历的时候我们又需要logn的时间来得到第若干个产生贡献的位置, 所以总的时间复杂度是O(n * logn * logn)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
bool flag[maxn];
int n, q, b[maxn], ans[maxn];
vector<int> add[2][maxn], qus[2][maxn];
int ADD[maxn * 4], ANS[maxn * 4], mark[maxn * 4];
void build(int* tree, int* marks, int u, int l, int r)
{
if(marks != NULL) marks[u] = 0;
if(l == r) tree[u] = 0;
else
{
tree[u] = 0;
int mid = (l + r) >> 1;
build(tree, marks, u << 1, l, mid);
build(tree, marks, u << 1 | 1, mid + 1, r);
}
}
//清空
void clean()
{
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j < 2; ++j)
{
add[j][i].clear();
qus[j][i].clear();
}
}
for(int i = 1; i <= q; ++i)
{
ans[i] = 0;
flag[i] = false;
}
build(ADD, NULL, 1, 1, q);
build(ANS, mark, 1, 1, q);
}
//单点更新
void ADD_update(int u, int l, int r, int x, int val)
{
if(l == r) ADD[u] = val;
else
{
int mid = (l + r) >> 1;
if(x <= mid) ADD_update(u << 1, l, mid, x, val);
else ADD_update(u << 1 | 1, mid + 1, r, x, val);
ADD[u] = ADD[u << 1] + ADD[u << 1 | 1];
}
}
//得到第k个有效的位置
int ADD_queryk(int u, int l, int r, int k)
{
if(l == r) return l;
else
{
int mid = (l + r) >> 1;
if(k <= ADD[u << 1]) return ADD_queryk(u << 1, l, mid, k);
else return ADD_queryk(u << 1 | 1, mid + 1, r, k - ADD[u << 1]);
}
}
void pushdown(int u, int l, int r)
{
if(mark[u])
{
int mid = (l + r) >> 1;
mark[u << 1] += mark[u];
mark[u << 1 | 1] += mark[u];
ANS[u << 1] += (mid - l + 1) * mark[u];
ANS[u << 1 | 1] += (r - mid) * mark[u];
mark[u] = 0;
}
}
//单点置零
void ANS_SET_update(int u, int l, int r, int x)
{
if(l == r)
{
mark[u] = 0;
ANS[u] = 0;
}
else
{
pushdown(u, l, r);
int mid = (l + r) >> 1;
if(x <= mid) ANS_SET_update(u << 1, l, mid, x);
else ANS_SET_update(u << 1 | 1, mid + 1, r, x);
ANS[u] = ANS[u << 1] + ANS[u << 1 | 1];
}
}
//区间更新
void ANS_ADD_update(int u, int l, int r, int ul, int ur, int val)
{
if(ul <= l && r <= ur)
{
ANS[u] += (r - l + 1) * val;
mark[u] += val;
}
else
{
pushdown(u, l, r);
int mid = (l + r) >> 1;
if(ul <= mid) ANS_ADD_update(u << 1, l, mid, ul, ur, val);
if(mid < ur) ANS_ADD_update(u << 1 | 1, mid + 1, r, ul, ur, val);
ANS[u] = ANS[u << 1] + ANS[u << 1 | 1];
}
}
//单点询问
int ANS_query(int u, int l, int r, int x)
{
if(l == r) return ANS[u];
else
{
pushdown(u, l, r);
int mid = (l + r) >> 1, res = 0;
if(x <= mid) res = ANS_query(u << 1, l, mid, x);
else res = ANS_query(u << 1 | 1, mid + 1, r, x);
return res;
}
}
int main()
{
while(~scanf("%d%d", &n, &q))
{
for(int i = 1; i <= n; ++i)
{
scanf("%d", b + i);
}
for(int i = 1; i <= q; ++i)
{
int l, r;
char operation[15];
scanf("%s%d%d", operation, &l, &r);
if(operation[0] == 'a')
{
// 对应addL和addR
add[0][l].push_back(i);
add[1][r].push_back(i);
}
else
{
flag[i] = true;
//询问也要有所标记
qus[0][l].push_back(i);
qus[1][r].push_back(i);
}
}
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j < add[0][i].size(); ++j)
{
//ADD树就是TADD,这里是将addL[j]加入到TADD[i]中
ADD_update(1, 1, q, add[0][i][j], 1);
}
for(int j = 0; j < qus[0][i].size(); ++j)
{
//将TQUE[i]中现在遇到左端点的对应询问清零
ANS_SET_update(1, 1, q, qus[0][i][j]);
}
for(int j = b[i]; ADD[1] >= j; j += b[i])
{
//以b[i]步长遍历TADD[i]
int tid = ADD_queryk(1, 1, q, j);
ANS_ADD_update(1, 1, q, tid, q, 1);
}
for(int j = 0; j < add[1][i].size(); ++j)
{
//遇到add的右端点,将其弹出TADD[i]
ADD_update(1, 1, q, add[1][i][j], 0);
}
for(int j = 0; j < qus[1][i].size(); ++j)
{
//遇到询问的右端点, 表示该询问已处理完毕, 可以得到答案
int p = qus[1][i][j], temp = ANS_query(1, 1, q, p);
ans[p] = temp;
}
}
for(int i = 1; i <= q; ++i)
{
if(flag[i]) printf("%d\n", ans[i]);
}
clean();
}
return 0;
}
写在最后
其实弄完之后觉得, 我这个思路其实不怎么巧妙, 但是胜在可以一步一步根据题目给出的信息推出做法和完善细节, 思维性比较差, 感觉比较适合我们这种脑子不太灵光的选手按部就班的进行推理解题。 还有就是, 离线计算贡献这种操作有时候又确实有奇效。