QOJ5173 染色
涉及知识点:扫描线,贪心
前言
非常好的一道题,转化十分精彩,之前完全没想到能有接近线性的做法。
题意
有一串纸条,上有 \(n\) 个格子,每个格子最开始有某种初始颜色,你可以用一秒钟将某个格子染成任意颜色,或者向左向右移动,前提是移动前后的格子颜色相同。有 \(q\) 组询问,每次询问从 \(u\) 到 \(v\) 的最短时间,询问之间独立。
\(n,q\leq 10^6\)。
思路
转化
正推没有什么头绪,我们可以反过来想,移动 \(len\) 距离最多可能需要的时间为 \(2len-2\),其中 \(len-1\) 是移动操作,另外 \(len-1\) 是染色操作,表示每一次移动前都将当前格子染成下一个格子的颜色,不难用发现这样的策略,无论格子颜色如何都一定能够成功抵达终点。于是在这个基础上考虑如何使步数更小,我们发现如果其中有一对 \(x<y\) 满足 \(col_x=col_y\),此时总步数就可以减小 \(1\),我们可以将 \([x+1,y-1]\) 的格子都染为 \(col_x\) 然后直接移动,这样从 \(x\) 移动到 \(y\) 就只需要染色 \(y-x-1\) 次,但按照我们之前的策略会染 \(y-x\) 次。
ABCA
之前的策略:
ABCA \(\rightarrow\) BBCA \(\rightarrow\) BBCA \(\rightarrow\) BCCA \(\rightarrow\) BCCA \(\rightarrow\) BCAA \(\rightarrow\) BCAA(共 \(6\) 步)
优化策略:
ABCA \(\rightarrow\) AACA \(\rightarrow\) AAAA \(\rightarrow\) AAAA \(\rightarrow\) AAAA \(\rightarrow\) AAAA(共 \(5\) 步)
子问题:拆子区间
因此我们的问题就转化为了:给定一段区间,求区间里最多能拆出多少子区间,满足子区间两端点颜色相同,并且子区间之间不交(端点可交)。
这听起来似乎有点经典,事实上,如果没有多次询问的限制,这是一个简单的线性 DP:
线性 DP:
\[f[i]=\max(\max\{f[j]\},\max\{f[k]+1\})\ (j,k<i,col[k]=col[i]) \]记 \(g[x]\) 为 \(col[k]=x\) 时最大的 \(f[k]+1\),\(maxf\) 为全局最大的 \(f[j]\) 即可简单优化为线性。
for(int i=1;i<=n;i++){ f[i]=max(maxf,g[a[i]]); maxf=max(maxf,f[i]); g[a[i]]=max(g[a[i]],f[i]+1); }
眼看 DP 是没什么办法优化下去了……但是我们发现,这题可以直接贪心!
怎么贪呢?对于一个点 \(x\),它的下一个区间选择它右侧区间中右端点最小的区间(这个区间的左端点可以为 \(x\),也可以大于 \(x\),但不能小于 \(x\)),将这个区间的右端点记为 \(fa[x]\)。不断从起点跳 \(fa[x]\),直至再跳就要超过终点为止,跳的步数就是最大子区间数。
比如说,\(1,2,2,3,2,3,5,1\) 的 \(fa\) 数组为 \(3,3,5,6,-1,-1,-1,-1\)。
为什么这样贪心对呢?因为 \(fa[x]\) 的设定相当于我们固定住了左边界 \(x\),于是当然是右边界越靠左越优。
\(fa\) 是可以线性求的,只需要先预处理出每个格子下一个与它颜色相同格子的编号 \(nxt\),注意此处 \(i\) 是倒序遍历:
转移方程表示我们每遍历到一个格子 \(i\) 就用区间 \([i,nxt[i]]\) 来更新一下答案。
离线询问
最后需要解决的问题,就是怎么处理这么多组询问。我们可以扫描线,当前扫到 \(r\),利用并查集维护某个点 \(l\) 能跳到的最大的 \(\leq r\) 的点,于是一组 \([l,r]\) 的询问的答案即为 \(l\) 跳多少次能跳到并查集的根,在求 \(fa\) 时顺便用一个类似后缀和的东西维护一下即可,具体可以看代码的 suf
数组。
细节
还有一个小细节,原问题并未保证起点 \(u \leq\) 终点 \(v\),不过根据我们以上分析,显然 \(u\) 到 \(v\) 和 \(v\) 到 \(u\) 答案是一样的,直接交换起点终点就可以了。
代码
#include<bits/stdc++.h>
using namespace std;
template<class T>inline void rd(T &x){
T res=0,f=1;
char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1; ch=getchar();}
while(isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
x=res*f;
}
template<class T>inline void wt(T x,char endch='\0'){
static char wtbuff[20];
static int wtptr;
if(x==0){
putchar('0');
}
else{
if(x<0){x=-x;putchar('-');}
wtptr=0;
while(x){wtbuff[wtptr++]=x%10+'0';x/=10;}
while(wtptr--) putchar(wtbuff[wtptr]);
}
if(endch!='\0') putchar(endch);
}
typedef pair<int,int> pii;
const int MAXN=1e6+5;
int n,q,a[MAXN],ans[MAXN];
int fa[MAXN],suf[MAXN],buck[MAXN],nxt[MAXN];
vector<int>son[MAXN];
struct QUERY{
int l,r,id;
QUERY(){}
QUERY(int a,int b,int c):l(a),r(b),id(c){}
};
vector<QUERY>qry[MAXN];
struct DSU{
int fa[MAXN];
int find(int x){
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
void init(int len){
for(int i=1;i<=len;i++){
fa[i]=i;
}
}
}dsu;
int main(){
rd(n);rd(q);
for(int i=1;i<=n;i++){
rd(a[i]);
}
int x,y;
for(int i=1;i<=q;i++){
rd(x);rd(y);
if(x>y) swap(x,y);
qry[y].emplace_back(x,y,i);
}
dsu.init(n+1);
for(int i=n;i>=1;i--){
if(!buck[a[i]]) nxt[i]=n+1;
else nxt[i]=buck[a[i]];
buck[a[i]]=i;
}
fa[n+1]=n+1;suf[n+1]=-1;
for(int i=n;i>=1;i--){
fa[i]=min(fa[i+1],nxt[i]);
son[fa[i]].push_back(i);
suf[i]=suf[fa[i]]+1;
}
for(int i=1;i<=n;i++){
for(auto it:son[i]){
dsu.fa[dsu.find(it)]=i;
}
for(auto it:qry[i]){
ans[it.id]=2*(it.r-it.l+1)-2-(suf[it.l]-suf[dsu.find(it.l)]);
}
}
for(int i=1;i<=q;i++){
wt(ans[i],'\n');
}
return 0;
}