[正睿集训2021] 无聊模拟赛5
总结
感觉最近学了很多东西,但是总喜欢把问题想得很复杂。
还有一些数据结构也是玄学,最近几场比赛已经让我不相信树套树和 \(k-d\) 树了。
Literary Trick
题目描述
给出两个字符串 \(s,t\),问他们的编辑距离是否小于等于 \(k\),如果小于等于 \(k\) 输出最小编辑距离。
\(s,t\) 的编辑距离定义如下,初始编辑串为 \(s\),每次可以进行如下三种操作的一种:
- 在当前字符串的某个位置插入一个字符。
- 删除当前字符串的某个字符。
- 将当前字符串的某个字符替换为另一个字符。
编辑距离即为把待编辑的字符串编辑为 \(t\) 所最少需要进行的操作数。
\(n,m\leq 5e5,k\leq 5000\)
解法
首先讲一下暴力 \(dp\),你发现题目中的操作都是针对单个字符的,所以一个一个字符的考虑,设 \(dp[i][j]\) 表示 \(s\) 的 \([1,i]\) 匹配 \(t\) 的 \([1,j]\) 所需要的最小步数,转移就考虑 \(s[i]=t[j]\) 是否成立,如果不成立就考虑三种操作,因为让 \(s,t\) 往同一个字符串靠等价于把 \(s\) 修改到 \(t\),所以可以只考虑添加字符的情况:
考试的时候怎么连 \(O(n^2)\) 都想不到啊
上面的做法状态数都爆了,不能通过的原因就是没有用到 \(k\leq 5000\),感性地理解在编辑次数很小的情况下 \(s,t\) 一定会有大段大段相同的部分,所以可以把 \(k\) 塞进状态里的同时跳过那些大段大段相同的。
设 \(f[i][j]\) 表示编辑次数为 \(i\) 的情况下,\(s[1,a-1]\) 匹配了 \(t[1,b-1]\),\(j=b-a\) 的最大的 \(a\),转移就考虑操作掉一个字符然后用 \(\tt lcp\) 来跳起走就行了:
- 在 \(s\) 后面加入 \(t[b]\),转移到 \(f[i+1][j+1]\)
- 在 \(t\) 后面加入 \(s[a]\),转移到 \(f[i+1][j-1]\)
- 使用替换操作,转移到 \(f[i+1][j]\)
用后缀数据加 \(st\) 表可以解决求 \(\tt lcp\),那么时间复杂度 \(O(n\log n+k^2)\)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 1000005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,x[M],y[M],c[M],h[M],sa[M],rk[M],dp[M][20];
int lg[M],f[5005][10005];char s[M],t[M];
void work(int n)
{
int m=300;
for(int i=1;i<=n;i++) ++c[x[i]=s[i]];
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) ++c[x[i]];
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
x[sa[1]]=1;num=1;
for(int i=2;i<=n;i++)
x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
if(num==n) break;
m=num;
}
int k=0;
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1;i<=n;i++)
{
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=n && j+k<=n && s[i+k]==s[j+k]) k++;
dp[rk[i]][0]=k;
}
for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
}
int lcp(int l,int r)
{
l=rk[l];r=rk[n+r+1];
if(l>r) swap(l,r);
l++;int k=lg[r-l+1];
return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
signed main()
{
n=read();m=read();k=read();
scanf("%s",s+1);scanf("%s",t+1);
if(n>m) swap(n,m),swap(s,t);
s[n+1]='#';//插入分隔符
for(int i=1;i<=m;i++) s[n+i+1]=t[i];
work(n+m+1);
f[0][k]=lcp(1,1)+1;
for(int i=0;i<k;i++)
for(int j=-i;j<=i;j++)
{
int a=f[i][j+k],b=a+j;
if(b<=m) f[i+1][j+k+1]=max(f[i+1][j+k+1],a+lcp(a,b+1));
if(a<=n) f[i+1][j+k-1]=max(f[i+1][j+k-1],a+1+lcp(a+1,b));
if(a<=n && b<=m) f[i+1][j+k]=max(f[i+1][j+k],a+1+lcp(a+1,b+1));
}
for(int i=0;i<=k;i++)
if(f[i][m-n+k]==n+1)
{
printf("%d\n",i);
return 0;
}
puts("-1");
}
Unfair Tournament
题目描述
\(a_i\) 表示第 \(i\) 个人的实力值,有 \(n-1\) 场比赛,每场比赛可以任意选择两个相邻的选手,实力值大的那个胜出,如果两个人实力值相同可以任意指定胜者,胜者的实力值 \(+1\)
问最后哪些选手可能是冠军。
\(n\leq 500000,0\leq a_i<2^{31}\)
解法
这道题我有点想讲三个方法。
首先不难发现如果我们钦定一个人为最后的胜者,那么我们肯定只让他去打架。如果比他弱的打架那么失去了提升实力值的机会,如果比他强的打架那么只会越来越强。
有一个比较玄学的乱搞做法,就是每个人判断的时候二分左边最多可以打到哪里,用 \(st\) 表查一下,扩展出去,然后操作右边。一直这么循环 \(20\) 次就可以通过此题,当然是数据水了,不过考试的时候也是推荐乱搞的。
第二个做法是 \(\tt Oneindark\) 的 \(cdq\) 分治,先做一个题意转化,如果一个人会失败那么一定是两个强者把他夹住了,即存在 \(l<x<r\) 使得 \(r-l-2+a_x<\min(a_l,a_r)\rightarrow r-l-1+a_x\leq\min(a_l,a_r)\),按照套路我们把 \(\min\) 拆开:
- 如果 \(a_l<a_r\),那么 \(a_x\leq a_l+l-r+1\),那么我们最小化 \(r\) 最大化 \(a_l+l\) 即可。
- 如果 \(a_l\geq a_r\),那么 \(a_x\leq a_r-r+l+1\),最大化 \(l\) 最大化 \(a_r-r\) 即可。
能不能 \(\tt cdq\) 分治呢?可以处理掉 \(a\) 的偏序关系,对于第一种情况,考虑枚举一个右半边的 \(r\) 找到最大的 \(l+a_l\) 然后把这个区间更新一下,但是你可能要问如果不选最大的 \(a_l+l\) 但是能让覆盖范围更广呢?没关系,我们只需要考虑 \(id> mid\) 这些位置的修改即可,剩下的我们可以在左半边做一次,找到最小的 \(r\) 解决 \(id\leq mid\) 这些位置即可。第二种情况同理。
时间复杂度 \(O(n\log n)\),这种做法的适用性特别广,值得借鉴。
分治一般看来是解决偏序问题的,但是他比你想得更强大。
好像很多看起来分治没有关系的东西用分治会比较好做,比如说这道题分治让 \(a_l+l-r+1\) 这个东西的最大化变得比较简单,主要是要有意识地往分治想。
为什么有些问题用分治会比较好做呢?我也不是特别清楚。
正解更为巧妙,所谓射人先射马,擒贼先擒王,根据这个理论 我们可以发现如果有一个人想要战胜 \([l,r]\) 里面所有的人,那么战胜其中的最大值 \(a[p]\) 就是很重要的。
看到和最大值有关的可以往笛卡尔树方面想,我们用最大值把一个区间 \([l,r]\) 划开然后递归下去:
- 如果 \(i\) 在 \(p\) 的左边,那么 \(a_i\geq a_p-(p-l-1)\) 就说明在战胜左半边的前提下可以战胜最大值。
- 如果 \(i\) 在 \(p\) 的右边,那么 \(a_i\geq a_p-(r-p-1)\) 就说明在战胜右半边的前提下可以战胜最大值。
不难发现一个点可以赢等价于对于它在笛卡尔树的所有祖先都满足条件,就相当于他一层一层地打上去嘛。那么构建出笛卡尔树,递归的时候记录一下最大值就可以做了。
时间复杂度 \(O(n\log n)\),因为要 \(\tt rmq\) 和排序。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
const int M = 500005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,a[M],ans[M],lg[M],dp[M][20];
int Min(int x,int y)
{
return a[x]>a[y]?x:y;
}
int get(int l,int r)
{
int k=lg[r-l+1];
return Min(dp[l][k],dp[r-(1<<k)+1][k]);
}
void solve(int l,int r,int mx)
{
if(l>r) return ;
int p=get(l,r);
if(a[p]>=mx) ans[++m]=p;
solve(l,p-1,max(mx,a[p]-(p-l-1)));
solve(p+1,r,max(mx,a[p]-(r-p-1)));
}
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read(),dp[i][0]=i;
if(i>1) lg[i]=lg[i>>1]+1;
}
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
dp[i][j]=Min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
solve(1,n,0);
sort(ans+1,ans+1+m);
printf("%d\n",m);
for(int i=1;i<=m;i++)
printf("%d ",ans[i]);
}
Lovely Painting
题目描述
给定一个 \(n\times m\) 的矩阵 \(A\),求有多少个矩形满足边界上都是同一种颜色。
\(n,m\leq 2000,0\leq a_{i,j}\leq 10^9\)
解法
考试时打了一个扫描线加 \(kd\) 树,因为 \(kd\) 树太辣鸡了我直接 \(\tt T\) 飞了,他吗的,再也不用 \(kd\) 树了。
我觉得这题比较难做的原因就是我枚举了相交的两个边界,所以限制条件会比较复杂。但是如果我们枚举相平行的边界就会简单许多,问题就在于我们不知道当前处理的矩形颜色是什么,因为这样枚举并没有一个交点。
那么套上一个分治,每次处理过分治中线的矩形,这样就得到了交点,进而知道了我们要统计矩形的颜色。
看吧,分治常常能解决一些意想不到的问题。
具体来说我们每次把分治一个子矩阵 \(r\times c\),要把它的某一维分成两半分治下去,但是为了保证复杂度我们要折长的那一维,这里就讨论折 \(x\) 维的情况。
枚举上下边界,对于每一对上下边界,左右分别都算出来然后乘法原理。预处理出每个点上下左右同色能延伸的距离,限制是在都能延伸到的 \(y\) 维中 \(x\) 维的颜色也是相同的,这个要看图才清楚:
看上去算这个怎么都要带 \(\log\),其实可以分两种情况讨论,如果下边界的 \(x\) 延伸长度更长那么上边界的所有位置都是可以取的,这时候做一个扫描线就可以了。否则我们枚举下边界往上扫描一遍就把另一种情况处理出来的。
由于每次面积至少折半所以时间复杂度 \(O(nm\log nm)\),要讨论 \(8\) 种情况,啊我死了
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 2005;
#define ll long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,a[M][M],up[M][M],dw[M][M],le[M][M],ri[M][M];
int s1[M][M],s2[M][M],del[M];ll ans;
void cdq(int xl,int xr,int yl,int yr)
{
if(xl==xr || yl==yr) return ;
if(xr-xl<yr-yl)
{
int mid=(yl+yr)>>1;
//printf("-------%d %d %d------\n",yl,mid,yr);
//左半边
for(int i=xl;i<=xr;i++)
{
int sum=0;
for(int j=xl;j<=xr;j++) del[j]=0;
//一开始还没取边界
for(int j=max(mid-le[i][mid],yl);j<=mid;j++)
{
sum++;
del[i+dw[i][j]+1]++;//在那里删除
}
for(int j=i+1;j<=xr;j++)
{
sum-=del[j];
if(le[j][mid]>=le[i][mid]) s1[i][j]+=sum;
}
}
for(int i=xl;i<=xr;i++)
{
int sum=0;
for(int j=xl;j<=xr;j++) del[j]=0;
for(int j=max(mid-le[i][mid],yl);j<=mid;j++)
{
sum++;
del[i-up[i][j]-1]++;//在那里删除
}
for(int j=i-1;j>=xl;j--)
{
sum-=del[j];
if(le[j][mid]>le[i][mid]) s1[j][i]+=sum;
}
}
//右半边
for(int i=xl;i<=xr;i++)
{
int sum=0;
for(int j=xl;j<=xr;j++) del[j]=0;
for(int j=mid+1;j<=min(mid+ri[i][mid],yr);j++)
{
sum++;
del[i+dw[i][j]+1]++;//在那里删除
}
for(int j=i+1;j<=xr;j++)
{
sum-=del[j];
if(ri[j][mid]>=ri[i][mid]) s2[i][j]+=sum;
}
for(int j=mid+1;j<=mid+ri[i][mid];j++)
del[i+dw[i][j]+1]=0;
}
for(int i=xl;i<=xr;i++)
{
int sum=0;
for(int j=xl;j<=xr;j++) del[j]=0;
for(int j=mid+1;j<=min(mid+ri[i][mid],yr);j++)
{
sum++;
del[i-up[i][j]-1]++;//在那里删除
}
for(int j=i-1;j>=xl;j--)
{
sum-=del[j];
if(ri[j][mid]>ri[i][mid]) s2[j][i]+=sum;
}
}
for(int i=xl;i<=xr;i++)
for(int j=i+1;j<=xr;j++)
{
//printf("%d %d ->%d %d\n",i,j,s1[i][j],s2[i][j]);
ans+=1ll*s1[i][j]*s2[i][j];
s1[i][j]=s2[i][j]=0;
}
cdq(xl,xr,yl,mid);cdq(xl,xr,mid+1,yr);
}
else
{
int mid=(xl+xr)>>1;
//上半边
for(int i=yl;i<=yr;i++)
{
int sum=0;
for(int j=yl;j<=yr;j++) del[j]=0;
for(int j=max(xl,mid-up[mid][i]);j<=mid;j++)
{
sum++;
del[i+ri[j][i]+1]++;
}
for(int j=i+1;j<=yr;j++)
{
sum-=del[j];
if(up[mid][i]<=up[mid][j]) s1[i][j]+=sum;
}
}
for(int i=yl;i<=yr;i++)
{
int sum=0;
for(int j=yl;j<=yr;j++) del[j]=0;
for(int j=max(xl,mid-up[mid][i]);j<=mid;j++)
{
sum++;
del[i-le[j][i]-1]++;
}
for(int j=i-1;j>=yl;j--)
{
sum-=del[j];
if(up[mid][i]<up[mid][j]) s1[j][i]+=sum;
}
}
//下半边
for(int i=yl;i<=yr;i++)
{
int sum=0;
for(int j=yl;j<=yr;j++) del[j]=0;
for(int j=mid+1;j<=min(xr,mid+dw[mid][i]);j++)
{
sum++;
del[i+ri[j][i]+1]++;
}
for(int j=i+1;j<=yr;j++)
{
sum-=del[j];
if(dw[mid][i]<=dw[mid][j]) s2[i][j]+=sum;//符号打反啦
}
}
for(int i=yl;i<=yr;i++)
{
int sum=0;
for(int j=yl;j<=yr;j++) del[j]=0;
for(int j=mid+1;j<=min(xr,mid+dw[mid][i]);j++)
{
sum++;
del[i-le[j][i]-1]++;
}
for(int j=i-1;j>=yl;j--)
{
sum-=del[j];
if(dw[mid][i]<dw[mid][j]) s2[j][i]+=sum;
}
}
for(int i=yl;i<=yr;i++)
for(int j=i+1;j<=yr;j++)
{
ans+=1ll*s1[i][j]*s2[i][j];
s1[i][j]=s2[i][j]=0;
}
cdq(xl,mid,yl,yr);cdq(mid+1,xr,yl,yr);
}
}
signed main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
a[i][j]=read()+1;
if(a[i][j]==a[i-1][j]) up[i][j]=up[i-1][j]+1;
if(a[i][j]==a[i][j-1]) le[i][j]=le[i][j-1]+1;
}
for(int i=n;i>=1;i--)
for(int j=m;j>=1;j--)
{
if(a[i][j]==a[i+1][j]) dw[i][j]=dw[i+1][j]+1;
if(a[i][j]==a[i][j+1]) ri[i][j]=ri[i][j+1]+1;
}
cdq(1,n,1,m);
printf("%lld\n",ans);
}