LuoguP7636 题解
\(1)\) 当 \(n\le 100,m\le3500\) 时
采用直接模拟的方法。由于版本之间需要时时切换,考虑用结构体直接存下一个版本,每次 save 时放入 vector 中,方便 load 时的查找。同时记录当前的版本,做 paint 操作时直接在当前版本上修改即可。
for(int i=1;i<=m;i++)
{
char s[8];
scanf("%s",s);
if(s[0]=='P')paint();
else if(s[0]=='S')
his.push_back(now);
else if(s[0]=='L')
{
int x;scanf("%d",&x);
now=his[x-1];
}
}
\(2)\) 当 \(n\le 1000,m\le 10^5\) 时
在画面的一次次修改、保存、回档中,我们发现有些操作是没必要进行的。如样例 \(2\) 中,在进行 LOAD 1
操作后,第二次未保存的 paint 操作便成了无效操作。
但是否在读入过程中就能判断出哪些操作是无效的呢?答案是不能的。考虑下面一系列操作:
PAINT
SAVE
PAINT
SAVE
LOAD 1
PAINT
LOAD 2
在进行 LOAD 1
操作后,虽然回到了第一次存档时的画面,但后面的 LOAD 2
操作回到第二次存档时的画面。而在两次存档之间的操作,虽然在 LOAD 1
时不会用到,但仍对答案造成影响。
因此从前往后扫无法判断出不需要哪些操作,而从后往前扫就能得到有效的染色指令。如上面的例子中,我们先遇到 LOAD 2
然后跳至第二处存档,继续往前扫,最后得出只有第一和第二次染色是有效的。
for(int i=1;i<=m;i++)
{
qq[i].init();
if(qq[i].s[0]=='S')
sav[++cnt]=i;// 记录第cnt次存档对应的位置,便于跳回
}
for(int i=m;i>0;i--)
{
if(qq[i].s[0]=='S')continue;// 跳过存档处
if(qq[i].s[0]=='L')i=sav[qq[i].to];
if(qq[i].s[0]=='P')paint(qq[i]);
}
注意到上述代码直接在从后往前扫时就进行了有效的染色操作。思考一下,如果这些操作从前往后去做,前面染过的颜色可能被后面重新染而改变,仍然需要一个一个去染,如果所有的操作都为 PAINT x 0 0 999 999
,单次染色的复杂度就高达 \(O(5\times 10^5)\),无法承受。
而从后往前去染色,我们只需要染上没有被染过的区域,从而减少重复染色所带来的巨大复杂度。而如何跳过被染过的区域,到达下一个未染过且需要被染色的区域呢?这里使用并查集来实现这一操作。
用并查集来维护每行中每个格子的下一个未被染色的格子。初始化中下一个未染色的即为本身,而在当前该格子被染后,我们需要与下一个可能被染色的格子建立联系,即 li[i].Union(j,j+2)
,通过语句 j=li[i].find(j)
来到达下一个未被染色的点,从而实现操作。
void paint(query zo)
{
int x=zo.x,xx=zo.xx,y=zo.y,yy=zo.yy,c=zo.c;
for(int i=x;i<=xx;i++)
for(int j=li[i].find(y+((i-x)&1));j<=yy;j=li[i].find(j))
{
co[i][j]=c;
li[i].Union(j,j+2);
}
return;
}
注意:在语句 li[i].Union(j,j+2)
中,\(j+2\) 可能要大于 \(n-1\),如果在并查集的初始化中不对 \(n\) 和 \(n+1\) 两个区域进行初始化,就会导致 \(j\) 可能成为 \(0\) 而陷入死循环。
AC Code 代码中根据个人习惯坐标都 \(+1\)。