2021杭电多校第一场1010.zoto
原题地址:Problem - 6959 (hdu.edu.cn)
题目大意:给出一些整数点 ( i , f [ i ] ) ( 1 ≤ i ≤ n ) (i,f[i])(1\leq i \leq n) (i,f[i])(1≤i≤n), q q q次询问,每次询问一个矩形范围有多少个y轴坐标不同的点。
1 ≤ n , q ≤ 1 0 5 1 \leq n,q \leq10^5 1≤n,q≤105
问题转化:
该问题可以转化为:
有一个长度为n的序列,q次询问,每次询问 [ l , r ] [l,r] [l,r]区间内数值在 [ a , b ] [a,b] [a,b]之间的不同数的个数。
该问题的做法和 这篇博客论述的 几乎一模一样,只是询问变得更简单了。
容易证明这种莫队加分块的做法复杂度为:
O
(
n
∗
n
)
O(n*\sqrt n)
O(n∗n)
莫队分块AC代码:
#include <bits/stdc++.h>
#define debug(x) cout<<#x"=" <<x<<'\n';
using ll=long long;
using namespace std;
int block;///块的大小,记得计算
const int M=1e5+5;
struct BLOCK
{
int sum[M],a[M];
void add(const int &x,const int &u) ///在x的位置加上y
{
sum[x/block]+=u;
a[x]+=u;
}
int ask(const int &l,const int &r)///询问[l,r]的和
{
int ans=0;
if(r-l<=block)
{
for(int i=l;i<=r;i++) ans+=a[i];
return ans;
}
int bl=(l/block+1)*block;
int br=r/block*block;
for(int i=l;i<bl;i++) ans+=a[i];
for(int i=br;i<=r;i++) ans+=a[i];
int blockl=l/block+1;
int blockr=r/block-1;
for(int i=blockl;i<=blockr;i++) ans+=sum[i];
return ans;
}
void clear()
{
memset(a,0,sizeof(a));
memset(sum,0,sizeof(sum));
}
}B;
struct node
{
int l,r,id,a,b;
bool operator < (const node &cmp) const
{
if(l/block == cmp.l/block) return r < cmp.r;
return l/block < cmp.l/block;
}
} q[M];
int num[M],a[M],hs[M];
void add(int x)
{
x=a[x];
if(hs[x]==0) B.add(x,1);
hs[x]++;
}
void del(int x)
{
x=a[x];
if(hs[x]==1) B.add(x,-1);
hs[x]--;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(hs,0,sizeof(hs));
B.clear();
int n,m;
scanf("%d%d",&n,&m);
block=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&q[i].l,&q[i].a,&q[i].r,&q[i].b);
q[i].id=i;
}
sort(q+1,q+m+1);
int l = 1,r = 0;
for(int i = 1; i <= m; i ++)
{
while(r < q[i].r) add(++r);
while(r > q[i].r) del(r--);
while(l < q[i].l) del(l++);
while(l > q[i].l) add(--l);
num[q[i].id]=B.ask(q[i].a,q[i].b);
}
for(int i = 1; i <= m; i ++) printf("%d\n",num[i]);
}
return 0;
}
思维历程:
对于该题我想到了一种比较有意思的做法:
从左到右建立线段树,线段树开在值域上,插入的是下标,每个节点都记录一下。
接下来用样例来解释建树过程:
1
4 2
1 0 3 1
1 0 4 3(询问 [ 1 , 4 ] [1,4] [1,4]中有多少个值在 [ 0 , 3 ] [0,3] [0,3]中)
1 0 4 2
序列是 1 0 3 1
先将(1,1)插入到树中:(第一个值为值,第二个为下标)
接下来插入(0,2)
接下来插入(3,3)
接下来应该插入(1,4),但是由于1的位置以已经有值了,所以要先删掉(保证无重)
然后再插入(1,4)
提前将询问按照r进行记录,当插入 ( a [ r i ] , r i ) (a[r_i],r_i) (a[ri],ri)之后就进行就进行询问,由于已经插入的下标是在 [ 1 , r i ] [1,r_i] [1,ri]范围内,所以我们只需要关心值域和和 l i l_i li就行。
实际上就是询问,[a,b]所包含的这些节点上下标大于等于 l i l_i li的有多少个
向下递归时,只要一个节点所表示的值域完全被询问的值域所覆盖就可以直接返回结果。(和普通线段树的区间求和一样)
对于该线段树的节点需要一个数据结构S
要能过支持n次增加和删除一个数,n次询问S中有多少个大于等于x的数,并且空间上要能开的下 n ∗ log n n*\log n n∗logn个S,所有S的元素总和为 n ∗ log n n*\log n n∗logn
S选取动态开点的权值线段树
那么增删查的复杂度都是log级别
总的复杂度为
O
(
n
∗
log
n
∗
l
o
g
n
)
O(n*\log n* log n)
O(n∗logn∗logn)
可惜的是,这样做的常数略大,无法通过该题,实际测试的时间约为上个做法的两倍。
常数原因分析如下:
- 对于每个节点有可能要执行一次删除和一次添加
- 莫队分块做法就是跑循环累加,几乎没有常数,跑的很快,而这种做法是很多层的递归,内含很多判断和计算。
值得一提的是该做法的空间复杂度也是
O( n ∗ log n ∗ log n n*\log n*\log n n∗logn∗logn)
树套树代码:
#include <bits/stdc++.h>
#define debug(x) cout << #x "=" << x << '\n';
using ll = long long;
using namespace std;
const int M = 1e5 + 10;
struct query
{
int l, r, a, b, id;
};
vector<query> Ask[M];
int a[M];
struct Segment_tree
{
struct node
{
int l, r, data; //l r表示左儿子和右儿子
};
static node t[M * 200];
static int tot;
const int lM, rM; //值域
int root; //根节点编号
Segment_tree(int _lM = 0, int _rM = M) : lM(_lM), rM(_rM)
{
root = ++tot;
}
void add_dfs(int x, int v, int p, int l, int r) //在x的位置上加v,当前节点是p
{
if (l == r)
{
t[p].data += v;
return;
}
int mid = (l + r) >> 1;
if (x <= mid)
{
if (t[p].l == 0)
t[p].l = ++tot;
add_dfs(x, v, t[p].l, l, mid);
}
else
{
if (t[p].r == 0)
t[p].r = ++tot;
add_dfs(x, v, t[p].r, mid + 1, r);
}
t[p].data = t[t[p].l].data + t[t[p].r].data;
}
void add(int x, int v)
{
add_dfs(x, v, root, lM, rM);
}
ll ask_dfs(int x, int p, int l, int r)
{
if (l >= x)
{
return t[p].data;
}
if (r < x)
{
return 0;
}
int mid = (l + r) >> 1;
ll ans = 0LL;
if (t[p].l)
ans += ask_dfs(x, t[p].l, l, mid);
if (t[p].r)
ans += ask_dfs(x, t[p].r, mid + 1, r);
return ans;
}
ll ask(int x) ///询问大于等于x的有多少个
{
return ask_dfs(x, root, lM, rM);
}
};
int Segment_tree::tot = 0;
Segment_tree::node Segment_tree::t[M * 200] = {};
//static 初始化
struct tree
{
int l, r;
Segment_tree data;
} t[4 * M];
void build(int p, int l, int r)
{
t[p].r = r, t[p].l = l;
if (r == l)
{
return;
}
int mid = (r + l) / 2;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
}
ll ask(int p, int x, int l, int r)
{
if (t[p].l >= l && t[p].r <= r)
{
return t[p].data.ask(x);
}
int mid = (t[p].l + t[p].r) / 2;
ll ans = 0;
if (l <= mid)
ans += ask(2 * p, x, l, r);
if (r > mid)
ans += ask(2 * p + 1, x, l, r);
return ans;
}
void changeone(int p, int x, int v, int f)
{
t[p].data.add(v, f);
if (t[p].l == t[p].r)
{
return;
}
int mid = (t[p].l + t[p].r) / 2;
if (x <= mid)
changeone(2 * p, x, v, f);
else
changeone(2 * p + 1, x, v, f);
}
int hs[M];
int ans[M];
int main()
{
//freopen("1.in","r",stdin);
//freopen("1.out","w",stdout);
//cout << sqrt(1e5);
build(1, 0, M);
int T;
scanf("%d", &T);
while (T--)
{
for (int i = 0; i < M; i++)
Ask[i].clear();
// memset(hs,0,sizeof(hs));
// for(int i=0; i<=Segment_tree::tot; i++)
// {
// Segment_tree::t[i].data=0;
// Segment_tree::t[i].l=0;
// Segment_tree::t[i].r=0;
// }
for (int i = 0; i < M; i++)
{
if (hs[i])
changeone(1, i, hs[i], -1);
hs[i] = 0;
}
//cout << Segment_tree ::tot << '\n';
//Segment_tree::tot = 4 * M;
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= m; i++)
{
int l, r, a, b;
scanf("%d%d%d%d", &l, &a, &r, &b);
Ask[r].push_back({l, r, a, b, i});
}
for (int i = 1; i <= n; i++)
{
if (hs[a[i]])
{
changeone(1, a[i], hs[a[i]], -1);
}
changeone(1, a[i], i, 1);
hs[a[i]] = i;
for (auto it : Ask[i])
{
ans[it.id] = ask(1, it.l, it.a, it.b);
}
}
for (int i = 1; i <= m; i++)
{
printf("%d\n", ans[i]);
}
}
return 0;
}
/*
1
10 1
1 6 8 7 2 1 4 1 4 8
3 1 9 2
*/