题解:P9361 [ICPC 2022 Xi'an R] Contests
引言
思路上,其它题解已经讲得很清楚了,就是预处理再倍增。但是具体的转移方程和代码实现迄今为止却没有一篇题解讲清楚,导致本蒟蒻狂调一整天。
这里我参考 @under_the_time 的题解里的处理方式,讲一讲相关状态的定义和转移、预处理和倍增的具体过程是怎样的。
前置
名词解释
先解释一下将要用到的名词:
- 元素/点:均指某个人;
- 位置:某一个排行榜内的一个排名;
- 组:一个排行榜;
- 排名高/低:在排行榜内的位置靠前/后。
- 跳/走
x 步:将题目看成“每次可以跳一步,x 可以跳到y 当且仅当存在一个序列使得y 出现在x 后面,问最少跳几步。”(来源)
结构体
首先,定义一个结构体类型 Rank,内含一个大小为
struct Rank{
int c[M];
};
我们要让它支持两种操作:比较和合并。
先说合并。在预处理过程中,我们会得到某个点通过某种单一路径可以得到不同的最高排名信息。而这些信息需要合并成一个完整的排名信息,即这个点通过所有路径所能到达每组的最高排名。
合并的代码也很简单,直接取各个方式的最高排名即可,本文中重载为加号 +:
Rank operator + (const Rank &x,const Rank &y)
{
Rank res;
for(int i=1;i<=m;i++)
res.c[i]=min(x.c[i],y.c[i]);
return res;
}
void operator += (Rank &x,const Rank &y){x=x+y;}
(其实这里重载的加号更像是一个
再说比较。在倍增过程中,我们要判断某个点
bool operator < (const Rank &x,const Rank &y) //x能够一步走到y当且仅当x<y
{
for(int i=1;i<=m;i++)
if(x.c[i]<y.c[i]) return true;
return false;
}
以上单次操作时间复杂度均为
状态表示
然后是倍增的状态表示(姑且先这么叫)。
以上三个状态都是 Rank 类型。
(迄今为止所有题解都没有提到需要用
解释:
特别注意
本题千万不要把不同组内同一个数的来回切换看做一步!
(这个错误想法导致我挂了很多次,甚至写这篇题解时也差点又这么想。)
预处理
g 数组
对于倍增的预处理,首先需要计算
(求和符号
for(int i=1;i<=m;i++)
for(int j=n;j>=1;j--)
{
if(j==n) g[i][j]=pos[a[i][j]];
else g[i][j]=pos[a[i][j]]+g[i][j+1];
}
f(i,0) 初始化
有了
这里面的
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(j==1) f[i][0]=g[1][pos[i].c[1]];
else f[i][0]+=g[j][pos[i].c[j]];
}
剩下的 f
最后就可以求出所有
该方程中
解释一下为什么只用记录最高排名就行了。因为这里方程所转移的只是将这个最高排名点作为中转点,从而便于走到该组内其它点的(以最高点走向组内其它点一定最优)。
反之,若某条路径无需以它作为中转点就可直达,那么这条路径的贡献应该在枚举这个中转点之前就已经计算完毕。如果加上中转点,多出来了一段长度,比已有的路径更劣,也不会更新和影响现有答案。
提交记录:R203561810。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+5,M=10,logN=17;
int n,m,q,a[M][N];
struct Rank{
int c[M];
}pos[N],f[N][logN+5],g[M][N];
/*
* pos[i]:i在每组内的排名/一步后的可达后缀
* f[i][j]:i跳2^j步后在每一组的最高排名/可达后缀
* g[i][j]:第i组内j~n位置上的所有人在每一组的最高排名/可达后缀
*/
bool operator < (const Rank &x,const Rank &y) //x能够走到y当且仅当x<y
{
for(int i=1;i<=m;i++)
if(x.c[i]<y.c[i]) return true;
return false;
}
Rank operator + (const Rank &x,const Rank &y) //合并两个排名信息
{
Rank res;
for(int i=1;i<=m;i++)
res.c[i]=min(x.c[i],y.c[i]);
return res;
}
void operator += (Rank &x,const Rank &y){x=x+y;}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
scanf("%d",&a[i][j]);
pos[a[i][j]].c[i]=j;
}
for(int i=1;i<=m;i++)
for(int j=n;j>=1;j--) //确定 g
{
if(j==n) g[i][j]=pos[a[i][j]];
else g[i][j]=pos[a[i][j]]+g[i][j+1];
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) //确定 f[i][0]
{
if(j==1) f[i][0]=g[1][pos[i].c[1]];
else f[i][0]+=g[j][pos[i].c[j]];
}
for(int k=1;k<=logN;k++) //确定其余 f
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(j==1) f[i][k]=f[a[j][f[i][k-1].c[j]]][k-1];
else f[i][k]+=f[a[j][f[i][k-1].c[j]]][k-1];
}
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
int px,py; scanf("%d%d",&px,&py);
Rank x=pos[px],y=pos[py];
if(x<y) printf("1\n"); //特判一步即达
else
{
int ans=0;
for(int i=logN;i>=0;i--)
{
Rank nxt=x;
for(int j=1;j<=m;j++)
nxt+=f[a[j][x.c[j]]][i];
if(!(nxt<y)) ans+=1<<i,x=nxt; //nxt不能抵达y
}
if(ans>n) printf("-1\n"); //无解
else printf("%d\n",ans+2);
}
}
return 0;
}
后记
本人蒟蒻,即使知道了倍增思路以后也难以打出代码,最后因为被
但是,在写题解的过程中,我发现我的理解依然有诸多缺漏,状态的定义和转移仍有许多不完整甚至错误之处,所以这篇题解也经过多次修正,直到整个逻辑自洽,这整个过程也让我受益匪浅。
因为多次修改,本题解难免会有一些前后语言表述不通顺的情况,还请多多包涵、提出意见,谢谢!