逛画展 P1638 题解(树状数组)

P1638 逛画展 题解

这篇文章不是你想的那样简单。

这篇文章通过正解讲述了双指针技巧的应用,以及介绍了一种维护不可差分信息的树状数组。

题目传送门

本题解法非常多,这里先讲代码简单,个人认为最好的双指针。

0x00 前置知识

想看双指针:敏捷的思维即可

想看树状数组的(不正规)解法:加上对树状数组和 lowbit 的基本了解

0x01 双指针解法

注意到,由于区间连续,我们可以通过双指针的形式枚举闭区间 [ l , r ] [l,r] [l,r]

显然每一步可以从左往右枚举区间的右端点 r r r 。对于每一个右端点 r r r ,我们要通过去除区间内多余的(不止一幅的)大师的画。

由于枚举的上一个区间 [ l ′ , r ′ ] [l',r'] [l,r] 已经包含所有大师的画,我们只要右移上一步留在原地的左指针 l ′ l' l 而不需要重新从头遍历。这属于双指针的一个典型应用。

那么就可以依次求解每一个右端点的最优区间:(伪代码)

procedure best_gap()
{
    int l=1,r=1;
    区间 ans = Infinity;
	forall r in 1 to n:
		for (this r):
			if (区间 [l,r] 包含了所有大师的画)
				尽可能向右推上一步留在原处的 l
				ans=min(ans,区间[l,r]);
	return ans;
}

对于 O ( 1 ) O(1) O(1) 判断什么时候有了所有大师的画,可以用一个记录每个大师的作品出现次数的桶 bucket [ ] \text{bucket}[] bucket[] 和大师计数器 c n t cnt cnt 解决。

我们先扩张区间 [ l , r ] [l,r] [l,r] ,令每位大师的画都在其中出现。

然后,只要左端点画作对应的大师的画作多于一张,这张画就不必保留,可以右移左指针。

这一操作的伪代码:

procedure push_left_pointer(r)
{
	for (this r)//先更新右指针上的画作
		if (bucket[画作[r]的作者]==0)//这个大师没有出现过
			大师计数器++;
		bucket[右端点大师]++;
	while(bucket[画作[l]的作者]>1)//再右移左指针
		l++,bucket[画作[l]的作者]--;
}

代码实现如下。

#include <bits/stdc++.h>
using namespace std;
int n,m;
int a[1000005];
int vis[2005],cnt;
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int l=1,r=1,ansl=-1,ansr=1000001;
	for(r=1;r<=n;r++)
	{
		if(!vis[a[r]]) cnt++;
		vis[a[r]]++;
		while(vis[a[l]]>1) vis[a[l]]--,l++;
		if(cnt==m) 
			if(r-l<ansr-ansl)
				ansl=l,ansr=r;
	}
	cout<<ansl<<' '<<ansr; 
	return 0;
}

时间复杂度 O ( n ) O(n) O(n)

0x02 树状数组(歪解)

事先声明:这个解法虽然废但是可以省掉一个 1 0 6 10^6 106 的大数组

这一切都要从偶然间发现的一个性质说起。

考虑以下样例,取自一次对拍数据。

13 5
331454235124

Match Table:
  | 1  2  3  4  5 | L
 —|———————————————|——————
 1| 0  0  1  0  0 | -
 2| 0  0  2  0  0 | -
 3| 3  0  2  0  0 | -
 4| 3  0  2  4  0 | -
 5| 3  0  2  4  5 | -
 6| 3  0  2  6  5 | -
 7| 3  7  2  6  5 | 7-2=5
 8| 3  7  8  6  5 | 8-3=5
 9| 3  7  8  6  9 | 9-3=6
10| 10 7  8  6  9 | 10-6=4
11| 10 11 8  6  9 | 11-6=5
12| 10 12 8  6  9 | 12-6=6
13| 10 12 8  13 9 | 13-8=5

其中 Match Table [i] [j] 表示读入到第 i i i 幅画作后,大师 j j j 的画作最后一次出现的位置。不难发现, max ⁡ ( i ) − min ⁡ ( i ) \max(i)-\min(i) max(i)min(i) 最小那一行的行号,就是最优区间右端点的编号。

这其实就是双指针带来的性质的变形,因为在第 i i i 次求解最优区间后, max ⁡ ( 第 i 行 ) = r ,   min ⁡ ( 第 i 行 ) = l \max(第i行)=r,~\min(第i行)=l max(i)=r, min(i)=l

可惜我没想到正解,然而,我写了一个暴力程序模拟。

居然拿了 90 90 90 p t s pts pts

记录详情

#include <bits/stdc++.h>
using namespace std;
int n,m;
int arr[2005];
int Max()
{
	int x=-1;
	for(int j=1; j<=m; j++)
		x=max(x,arr[j]);
	return x;
}
int Min()
{
	int x=INT_MAX;
	for(int j=1; j<=m; j++)
		x=min(x,arr[j]);
	return x;
}
int main()
{
	scanf("%d %d",&n,&m);
	int x,l=9999,p;
	for(int i=1; i<=n; i++)
	{
		scanf("%d",&x);
		arr[x]=i;
		int A=Max(),B=Min();
		if(A-B<l && B!=0)
			l=A-B,p=B;
	}
	cout<<p<<' '<<p+l;
	return 0;
}

时间复杂度 O ( n m ) O(nm) O(nm) ,过不去很正常。

(但是空间复杂度奇迹般地变成了原来的约 1 500 \frac{1}{500} 5001 ?)

目前看来最能也最该优化的,就是 max ⁡ ( ) \max() max() min ⁡ ( ) \min() min() 函数了。每次都遍历一行来 RMQ 显然不现实。

由于在区间内部修改,所以单调栈和单调队列先不考虑;ST 表和笛卡尔树不带修;线段树不会写。一般的树状数组只能维护可差分可结合律的信息,而 RMQ 不满足可差分性质。

我真心不想写线段树,于是就有了这篇题解。

0x03 树状数组维护不可差分信息

整理自非常多的资料,不胜枚举,十分感谢各位神犇作者。

这里以 max ⁡ \max max 为例,记元素总数为 t o t tot tot 。数组下标从 1 1 1 开始,原数组为 a [ ] a[] a[] ,树状数组为 c [ ] c[] c[]

首先是建树操作,只需要将求和换成 max ⁡ \max max 即可。

void build()
{
	for(int i=1; i<=tot; i++)
	{
		for(int j=i-lowbit(i)+1; j<=i; j++)
			c[i]=max(c[i],a[j]);
	}
}

接着是查询 (即 queryMax() 函数)。

注意到,c[i] 维护的是 max ⁡ k = i − lowbit ( i ) + 1 i a [ k ] \max\limits_{k=i-\text{lowbit}(i)+1}^{i}a[k] k=ilowbit(i)+1maxia[k]

那么我们就可以得到
q u e r y ( i , j ) = max ⁡ { ( c [ j ] , q u e r y ( i , j − lowbit ( j ) ) )      j − lowbit ( j ) > i a [ j ] , q u e r y ( j − 1 )                         j − lowbit ( j ) < = i query(i,j)=\max \begin{cases} (c[j],query(i,j-\text{lowbit}(j)))~~~~j-\text{lowbit}(j)>i\\ a[j],query(j-1)~~~~~~~~~~~~~~~~~~~~~~~j-\text{lowbit}(j)<=i\\ \end{cases} query(i,j)=max{(c[j],query(i,jlowbit(j)))    jlowbit(j)>ia[j],query(j1)                       jlowbit(j)<=i
解释一下:当 j − lowbit ( j ) > i j-\text{lowbit}(j)>i jlowbit(j)>i 时,表明 c [ j ] c[j] c[j] 维护的区间最大值在 [ i , j ] [i,j] [i,j] 之内,递归求解。反之,则只能和 a [ j ] a[j] a[j] 进行比较,并将区间变成 [ i , j − 1 ] [i,j-1] [i,j1] 在更小范围内试图求解。

代码中改写为非递归形式求解。这里查询范围是固定的。

int queryMax(int l=1,int r = 0)
{
	r=tot;//在这里,类成员函数初始化时,不能同时以这个类的成员设默认值。
	int s=-1;
	while(r>=l)
	{
		s=max(a[r],s);
		r--;
		for(; r-lowbit(r)>=l; r-=lowbit(r))
			s=max(c[r],s);
	}
	return s;
	}

最后一步是更新。

结合 c [ i ] c[i] c[i] 的定义可得,一个元素 c [ x ] c[x] c[x] 可转移到 c [ i ] c[i] c[i],当且仅当 x + lowbit ( x ) = i x+\text{lowbit}(x)=i x+lowbit(x)=i

如下所示。

i = 1001000
可以直接或间接转移到 i 的元素:
1000100 + 100 = 1001000
1000010 +  10 = 1000100
1000001 +   1 = 1000010

反推可得 c [ i ] c[i] c[i] 只可能被 { c [ x ]   ∣   x ∈ { i − 2 k   ∣   2 k < l o w b i t ( i ) } } \{c[x]~|~x\in \{i-2^k~|~2^k<lowbit(i)\}\} {c[x]  x{i2k  2k<lowbit(i)}} 转移过来。

代码如下。

void update(int pos,int val)//更新 a[pos] 为 val
{
	a[pos]=val;
	int lpos;
	while(pos<=tot)
	{
		c[pos]=a[pos];
		lpos=lowbit(pos);
		for(int i=1; i<lpos; i<<=1)
			c[pos]=max(c[pos],c[pos-i]);
		pos+=lpos;
	}
}

处理最小值时,把上文的所有 max ⁡ \max max 换成 min ⁡ \min min ,将无穷小和无穷大对调即可。

0x04 具体解法

先求出第一个存在 L L L 的 Match Table(即,所有大师的画作第一次都出现时的位置表),用它分别建维护最大值和最小值的树状数组。

每一步修改就在树状数组里 update ( x , i ) \text{update}(x,i) update(x,i) ,然后求值取最优。

时间复杂度约为 O ( n log ⁡ 2 m ) O(n\log^2m) O(nlog2m)

通过共用全局数组,可以进一步减小空间占用。

#include <bits/stdc++.h>
using namespace std;
int n,m;
int a[2005];//模拟用的数组,这里共用了
struct fenwick_tree_max//维护区间Max
{
	int c[2005],tot;
	int lowbit(int x)
	{
		return x&-x;
	}
	void build()
	{
		for(int i=1; i<=tot; i++)
		{
			for(int j=i-lowbit(i)+1; j<=i; j++)
				c[i]=max(c[i],a[j]);
		}
	}
	int queryMax(int l=1,int r = 0)
	{
		r=tot;
		int s=-1;
		while(r>=l)
		{
			s=max(a[r],s);
			r--;
			for(; r-lowbit(r)>=l; r-=lowbit(r))
				s=max(c[r],s);
		}
		return s;
	}
	void update(int pos,int val)
	{
		a[pos]=val;
		int lpos;
		while(pos<=tot)
		{
			c[pos]=a[pos];
			lpos=lowbit(pos);
			for(int i=1; i<lpos; i<<=1)
				c[pos]=max(c[pos],c[pos-i]);
			pos+=lpos;
		}
	}
} f1;
struct fenwick_tree_min//维护区间Min
{
	int c[2005],tot;
	int lowbit(int x)
	{
		return x&-x;
	}
	void build()
	{
		for(int i=1; i<=tot; i++)
		{
			c[i]=a[i];
			for(int j=i-lowbit(i)+1; j<=i; j++)
				c[i]=min(c[i],a[j]);
		}
	}
	int queryMin(int l=1,int r = 0)
	{
		r=tot;
		int s=INT_MAX;
		while(r>=l)
		{
			s=min(a[r],s);
			r--;
			for(; r-lowbit(r)>=l; r-=lowbit(r))
				s=min(c[r],s);
		}
		return s;
	}
	void update(int pos,int val)
	{
		a[pos]=val;
		int lpos;
		while(pos<=tot)
		{
			c[pos]=a[pos];
			lpos=lowbit(pos);
			for(int i=1; i<lpos; i<<=1)
				c[pos]=min(c[pos],c[pos-i]);
			pos+=lpos;
		}
	}
} f2;
int main()
{
	scanf("%d %d",&n,&m);
	int x,l=INT_MAX,p,clk=m,i;
	for(i=1; clk!=0; i++)
	{
		scanf("%d",&x);
		if(clk!=0)
		{
			if(!a[x]) clk--;
			a[x]=i;
		}
	}
	f1.tot=m,f2.tot=m;
	f1.build();
	f2.build();
	int aa=f1.queryMax(),b=f2.queryMin();
	if(aa-b<l)
		p=b,l=aa-b;
	for(; i<=n; i++)
	{
		scanf("%d",&x);
		f1.update(x,i);
		f2.update(x,i);
		aa=f1.queryMax(),b=f2.queryMin();
		if(aa-b<l)
			p=b,l=aa-b;
	}
	cout<<p<<' '<<l+p;
	return 0;
}

AC 记录

空间占用理论上为双指针等解法的 1 167 \frac{1}{167} 1671

0x05 感想

考的还是思维。我是因为没想到双指针才采用动态 RMQ,大家平时一定要注意思考题目的性质,多打草稿。

一道黄题杀鸡用牛刀不太好,也希望大家能与我一起共同思考对新题进行分析的方法。

  • 36
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值