卡 常 战 神——P10436 [JOIST 2024] 卡牌收集 / Card Collection 题解
前言
这篇题解讲的是 DFA + 等价类分治做法,卡了两天的常但是加深了对很多东西的理解,可以在 LOJ 和洛谷上通过。长文警告,极度卡常,慎重尝试。
在更慢的 OJ 上需要使用指令集。
Part 1 提出算法
我们先来做一遍这个题。
先考虑一下单询问,发现我们并不关心每个卡牌的数值具体是多少,而只关心卡牌数值相对于目标的大小关系。如果卡牌数值比目标小就是 -1,相等是 0,大于是 +1,那么我们就可以把一张卡牌变成一个二元组 (-1/0/+1,-1/0/+1)。我们的目标就是在这种情况下去判定能否合成 (0,0)。
这类相邻合成问题是 DFA 的专长。我们将二元组看做 0~8 的字符集,然后定义出 9*9 的合并规则后,我们就可以通过暴力方式跑出能够识别合法串的 DFA。
具体地,我们根据 Myhill-Nerode 定理,只需要枚举一个小字符串集合 X,再枚举一个小字符串集合 Z,对于 X 中的任意两个串 \(X_1,X_2\),我们判定二者属于同一等价类(或者说自动机状态)的充要条件是,对于任意的 Z,\(X_1+Z\) 的接受性(或者说合法性)和 \(X_2+Z\) 的接受性都一致。
这里 X,Z 集合枚举的规模都是靠经验来定,只要你试了几个数发现增大 X/Z 并没有使自动机大小/转移边变化的时候就差不多建对了。
建自动机的代码可以参考另一篇题解。这里只贴出来建完的自动机。
const int idx=24;
const bool acc[idx+5]={0 ,0,0,0,0,0,1,0,0,0,0 ,1,0,1,0,0 ,0,0,1,0,0 ,0,0,0,0};
const int to[idx+5][9]={
{},
{2,3,4,5,6,7,8,9,2},//1
{2,3,10,5,6,7,12,9,2},//2
{3,3,3,18,18,14,15,15,3},//3-
{10,3,4,7,20,7,10,3,10},//4
{5,13,16,5,13,16,5,19,5},//5-
{11,11,17,11,11,11,22,11,11},//6-
{7,14,7,16,18,7,16,18,7},//7-
{12,9,12,5,21,5,8,9,12},//8
{9,15,15,19,13,13,9,9,9},//9-
{10,3,10,16,6,7,23,15,10},//10
{11,11,11,11,11,11,11,11,11},//11-
{12,15,23,5,6,16,12,9,12},//12
{11,11,11,11,11,11,22,11,11},//13-
{14,14,14,18,18,14,18,18,14},//14
{15,15,15,11,11,11,15,15,15},//15-
{16,11,16,16,11,16,16,11,16},//16-
{11,11,17,11,11,11,11,11,11},//17-
{11,11,17,11,11,11,11,11,11},//18-
{19,13,13,19,13,13,19,19,19},//19-
{20,14,20,18,18,14,24,18,20},//20
{21,13,24,19,13,13,21,19,21},//21
{11,11,11,11,11,11,22,11,11},//22-
{23,15,23,16,6,16,23,15,23},//23
{24,11,24,11,11,11,24,11,24}//24-
};
让我们先暂时把这个 DFA 当成黑盒,不去关心其内部转移的形式。
至此已经获得了 \(O(nq)\) 的暴力解法,将每个询问按顺序放进自动机里跑就行了。
Part 2 复杂度优化
以下设 \(|S|=24\) 为自动机规模。
我们发现这样一个事实:在长为 \(B\) 的区间中,本质不同的询问只有 \(O(B^2)\) 种。由于大小关系只有 <,=,> 三种,所以具体的数字是 \((2B+1)^2\) 种。
这告诉我们什么呢?我们如果以块长 \(B\) 为阈值分块,那么块内可以 \(O(B^2|S|)\) 预处理所有本质不同类的结果(一个类求出如果进去前在某状态,走了之后会到哪个状态),然后所有询问就只用 \(O(1)\) 推进一个块。
如何预处理呢?我们在同块内序列分治,先处理左半边的映射,再处理右半边,再进行合并。容易得到 \(T(n)=2T(\frac{n}{2})+O(n^2|S|)\),主定理分析这个就是 \(O(n^2|S|)\) 的。
由于 \(n,q\) 同级,分析一下很容易得到 \(O(n\sqrt{n|S|})\) 的平衡结果。计算一下是有通过的可能的。事实上,这就是大名鼎鼎的等价类分治的一种实现方式。
但是如果你直接写,很快会被打醒。
首先一个最大的问题是,在这种情况下你根本没法把 \(B\) 设的太大。不论空间开不开的下,即使你逐块处理,预处理的常数也特别特别大。经过试验,\(B=64\) 时预处理在洛谷上就要将近 3s,\(B=100\) 就已经无法在 4s 内跑完了。我们询问的复杂度是 \(O(\frac{qn}{B})\),这样的情况显然无法满足我们的需求,实际上也是。
说不定你可以把 20s 以上的东西优化直接冲过去,但我选择在算法层面进行卡常。
Part 3 卡常第一阶段:分治内卡常
我们抛弃阈值的想法,直接进行序列分治。
我们维护一个全局数组 \(now\) 表示某个询问当前所在的自动机状态。分治函数 solve(vector<> a,vector<> q) 执行后需要保证所有 \(q\) 内存储的询问都被正确地按顺序推进了 \(a\) 中的所有卡牌。
为了保证复杂度,我们需要在每一层都进行等价类的去重。具体地,我们在分治函数中,对于两维中的某一维,将 \(a\) 和 \(q\) 进行排序双指针式的离散化。离散化后,如果某些询问的 \((x,y,now_{id})\) 三元组(前两维数值,第三维表示当前在自动机上的状态)完全一致,我们就可以将其压成同一个询问往下递归。等递归回来再展开成原来的询问。
void solve(vector<arr> a,vector<arrr> q){
int n=a.size(),m=q.size();
if(n<=1){
arr v=a[0];
for(auto x:q){
int sx=(v[0]<x[0]?0:(v[0]==x[0]?1:2));
int sy=(v[1]<x[1]?0:(v[1]==x[1]?1:2));
now[x[2]]=to[now[x[2]]][sx*3+sy];
}return ;
}
vector<arr> prea(a);
for(int c:{0,1}){
int mxv=0;
for(auto x:a) mxv=max(mxv,x[c]);
for(auto x:q) mxv=max(mxv,x[c]);
vector<int> h(mxv+1,0);
int tot=-1,lst=-1;
srt(a,[&](const arr &x){return x[c];});
srt(q,[&](const arrr &x){return x[c];});
int j=0;
for(int i=0;i<m;i++){
while(j<n&&a[j][c]<q[i][c]){
++tot;
h[a[j][c]]=tot;lst=-1;j++;//??!!
}
if(lst==-1) ++tot;
h[q[i][c]]=tot;lst=q[i][c];
}while(j<n){
++tot;
h[a[j][c]]=tot;lst=-1;j++;
}
for(auto &x:prea) x[c]=h[x[c]];
for(auto &x:q) x[c]=h[x[c]];
}
vector<int> pre(q.size(),0);
vector<arrr> nxtq;
srt(q,[&](const arrr &x){return now[x[2]];});
for(int i=0,j;i<m;i=j+1){
j=i;while(j<m&&q[i][0]==q[j][0]&&q[i][1]==q[j][1]
&&now[q[i][2]]==now[q[j][2]]) j++;j--;
for(int k=i;k<=j;k++) pre[k]=i;
nxtq.push_back(q[i]);
}
int mid=(n-1)>>1;
vector<arr> la,ra;
for(int i=0;i<=mid;i++) la.push_back(prea[i]);
for(int i=mid+1;i<n;i++) ra.push_back(prea[i]);
solve(la,nxtq);solve(ra,nxtq);
for(int i=0;i<m;i++) now[q[i][2]]=now[q[pre[i]][2]];
}
上面的代码没有经过卡常,相对易读,没有看懂文字的可以看代码或者 AI 辅助理解。
这样我们每一个节点的复杂度是 \(O(\min(q,n^2|S|))\),仍然可以分析成 \(O(n\sqrt{n|S|})\),但是卡的并不满。此时实测时长 30s 左右。
优化 1:减少排序
我们发现我们对于整个 \(q\) 排序很不值得,我们只是想要 \(q\) 里的值和 \(a\) 里的值有序。因此,我们专门开一个有序的数组 qv[2] 来记录 \(q\) 中两维的数值,由于离散化并不改变其有序性,我们就可以省掉 \(q\) 的排序。
优化 2:去重优化
我们去重时依赖于将 \(q\) 的三维进行排序,但是现在要优化排序。我们考虑将 \(q\) 始终按照第一维排序,注意到后两维的上界分别是 \((2n+1,|S|)\),我们直接以 \(|S|\times y+now\) 作为其哈希就可以直接用一个数组存下。
然后将一些 vector 优化成静态 static 数组即可。
void solve(vector<arr> a,vector<arrr> q,array<vector<int>,2> qv){
int n=a.size(),m=q.size(),l[2]={(int)qv[0].size(),(int)qv[1].size()};
if(n<=1){
arr v=a[0];
for(auto x:q){
int sx=(v[0]<x[0]?0:(v[0]==x[0]?1:2));
int sy=(v[1]<x[1]?0:(v[1]==x[1]?1:2));
now[x[2]]=to[now[x[2]]][sx*3+sy];
}return ;
}
vector<arr> prea(a);
for(int c:{0,1}){
static int h[2*maxn];
int tot=-1,lst=-1;
srt(a,[&](const arr &x){return x[c];});
//qv has been sorted.
int j=0;
for(int i=0;i<l[c];i++){
while(j<n&&a[j][c]<qv[c][i]){
++tot;
h[a[j][c]]=tot;lst=-1;j++;//??!!
}
if(lst==-1) ++tot;
h[qv[c][i]]=tot;lst=qv[c][i];
}while(j<n){
++tot;
h[a[j][c]]=tot;lst=-1;j++;
}
for(auto &x:prea) x[c]=h[x[c]];
for(auto &x:q) x[c]=h[x[c]];
for(auto &x:qv[c]) x=h[x];
l[c]=unique(qv[c].begin(),qv[c].end())-qv[c].begin();
qv[c].resize(l[c]);
}
vector<int> pre(q.size(),0);
vector<arrr> nxtq;
static int hs[maxn*2*idx+idx+5];
for(int i=0,j;i<m;i=j+1){
j=i;while(j<m&&q[i][0]==q[j][0]) j++;j--;
for(int k=i;k<=j;k++){
int d=id(q[k]);
if(hs[d]==0){
nxtq.push_back(q[k]);
hs[d]=k;
}pre[k]=hs[d];
}for(int k=i;k<=j;k++) hs[id(q[k])]=0;
}
int mid=(n-1)>>1;
vector<arr> la,ra;
for(int i=0;i<=mid;i++) la.push_back(prea[i]);
for(int i=mid+1;i<n;i++) ra.push_back(prea[i]);
solve(la,nxtq,qv);solve(ra,nxtq,qv);
for(int i=0;i<m;i++) now[q[i][2]]=now[q[pre[i]][2]];
}
现在实测 13s 左右,递归 \(q\) 的总量 \(5\times 10^8\) 左右。
Part 4 卡常第二阶段:自动机卡常
刚才已经是我们在其他部分能卡到的极限了。现在我们必须拆开自动机的黑盒,去转移里面剪枝。
{2,3,4,5,6,7,8,9,2},//1
{2,3,10,5,6,7,12,9,2},//2
{3,3,3,18,18,14,15,15,3},//3-
{10,3,4,7,20,7,10,3,10},//4
{5,13,16,5,13,16,5,19,5},//5-
{11,11,17,11,11,11,22,11,11},//6-
{7,14,7,16,18,7,16,18,7},//7-
{12,9,12,5,21,5,8,9,12},//8
{9,15,15,19,13,13,9,9,9},//9-
{10,3,10,16,6,7,23,15,10},//10
{11,11,11,11,11,11,11,11,11},//11-
{12,15,23,5,6,16,12,9,12},//12
{11,11,11,11,11,11,22,11,11},//13-
{14,14,14,18,18,14,18,18,14},//14
{15,15,15,11,11,11,15,15,15},//15-
{16,11,16,16,11,16,16,11,16},//16-
{11,11,17,11,11,11,11,11,11},//17-
{11,11,17,11,11,11,11,11,11},//18-
{19,13,13,19,13,13,19,19,19},//19-
{20,14,20,18,18,14,24,18,20},//20
{21,13,24,19,13,13,21,19,21},//21
{11,11,11,11,11,11,22,11,11},//22-
{23,15,23,16,6,16,23,15,23},//23
{24,11,24,11,11,11,24,11,24}//24-
首先,对于 11 号点,我们发现其只有到 11 的自环转移,因此只要当前状态是 11 就没有转移的必要了,可以不加进 nxtq 中递归。
这样 sumq 就到了 4.4e8 的量级。
再继续观察,我们发现对于 15 号点,只要这段区间中有某个卡牌的 y 和当前询问相等,就一定会到 11 号点,否则一定留在 15 号点。这样你只要记下区间中 "是否存在某个卡牌的值等于 y" 就可以直接算出来并不将它扔进 nxtq 中。
同理,16 号点也能应用此优化。
加入后 sumq 就只有 2e8 左右了。再加入一些把 vector 改成静态数组,别的自动机人类智慧,或者阈值分块就可以通过了!
最后代码很长。时间复杂度 \(O(n\sqrt{n|S|})\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
const int idx=24;
const bool acc[idx+5]={0 ,0,0,0,0,0,1,0,0,0,0 ,1,0,1,0,0 ,0,0,1,0,0 ,0,0,0,0};
const int to[idx+5][9]={
{},
{2,3,4,5,6,7,8,9,2},//1
{2,3,10,5,6,7,12,9,2},//2
{3,3,3,18,18,14,15,15,3},//3-
{10,3,4,7,20,7,10,3,10},//4
{5,13,16,5,13,16,5,19,5},//5-
{11,11,17,11,11,11,22,11,11},//6-
{7,14,7,16,18,7,16,18,7},//7-
{12,9,12,5,21,5,8,9,12},//8
{9,15,15,19,13,13,9,9,9},//9-
{10,3,10,16,6,7,23,15,10},//10
{11,11,11,11,11,11,11,11,11},//11-
{12,15,23,5,6,16,12,9,12},//12
{11,11,11,11,11,11,22,11,11},//13-
{14,14,14,18,18,14,18,18,14},//14
{15,15,15,11,11,11,15,15,15},//15-
{16,11,16,16,11,16,16,11,16},//16-
{11,11,17,11,11,11,11,11,11},//17-
{11,11,17,11,11,11,11,11,11},//18-
{19,13,13,19,13,13,19,19,19},//19-
{20,14,20,18,18,14,24,18,20},//20
{21,13,24,19,13,13,21,19,21},//21
{11,11,11,11,11,11,22,11,11},//22-
{23,15,23,16,6,16,23,15,23},//23
{24,11,24,11,11,11,24,11,24}//24-
};
typedef array<int,2> arr;
typedef array<int,3> arrr;
template<typename T,typename Func>
void srt(vector<T> &a,Func id){
int n=a.size(),m=0;for(auto x:a) m=max(m,id(x));
static int cnt[2*maxn];
static T tmp[maxn];
for(int i=0;i<=m;i++) cnt[i]=0;
for(auto x:a) cnt[id(x)]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n-1;i>=0;i--) tmp[--cnt[id(a[i])]]=a[i];
for(int i=0;i<n;i++) a[i]=tmp[i];
}
//a[i][0/1]<=2*a.size()+1.
int now[maxn];
const int Bsmall=1;
const int maxdep=20;
inline int id(arrr x){return x[1]*24+now[x[2]]-1;}
long long debug_cnt_n,debug_cnt_q,debug_cnt_qv;
long long debug_cnt_15,debug_cnt_16;
void solve(int dep,
vector<arr> a,const vector<arrr> &qq,array<vector<int>,2> &qqv,
int qqsz){
int n=a.size(),m=qqsz,l[2]={(int)qqv[0].size(),(int)qqv[1].size()};
debug_cnt_n+=n,debug_cnt_q+=m,debug_cnt_qv+=l[0]+l[1];
if(n>10000) cerr<<"solve:"<<n<<' '<<m<<' '<<l[0]<<' '<<l[1]<<endl;
if(n <= Bsmall){
for(int i=0;i<n;i++){
arr v=a[i];
for(auto x:qq){
int sx=(v[0]<x[0]?0:(v[0]==x[0]?1:2));
int sy=(v[1]<x[1]?0:(v[1]==x[1]?1:2));
now[x[2]]=to[now[x[2]]][sx*3+sy];
}
}
return;
}
if(n>10000){
debug_cnt_q-=m;debug_cnt_qv-=l[0]+l[1];
vector<arr> la,ra;int mid=(n-1)>>1;
for(int i=0;i<=mid;i++) la.push_back(a[i]);
for(int i=mid+1;i<n;i++) ra.push_back(a[i]);
solve(dep+1,la,qq,qqv,(int)qq.size());solve(dep+1,ra,qq,qqv,(int)qq.size());
return ;
}
static arrr vecq[maxdep][maxn];
auto &q=vecq[dep];
for(int i=0;i<m;i++) q[i][2]=qq[i][2];
array<vector<int>,2> qv=qqv;
vector<arr> prea(a);
int mxv[2]={-1,-1};
static int h[2*maxn],isa[2][2*maxn];
for(int c:{0,1}){
int &tot=mxv[c];int lst=-1;
srt(a,[&](const arr &x){return x[c];});
//qv has been sorted.
int j=0;
for(int i=0;i<l[c];i++){
while(j<n&&a[j][c]<qv[c][i]){
++tot;
h[a[j][c]]=tot;
lst=-1;j++;//??!!
}
if(lst==-1) ++tot;
h[qv[c][i]]=tot;lst=qv[c][i];
}while(j<n){
++tot;
h[a[j][c]]=tot;
lst=-1;j++;
}
for(auto &x:prea) x[c]=h[x[c]],isa[c][x[c]]=1;
for(int k=0;k<m;k++) q[k][c]=h[qq[k][c]];
for(auto &x:qv[c]) x=h[x];
l[c]=unique(qv[c].begin(),qv[c].end())-qv[c].begin();
qv[c].resize(l[c]);
}
// for(auto x:prea) printf("afta:%d %d\n",x[0],x[1]);
// for(auto x:q) printf("aftq:%d %d %d\n",x[0],x[1],now[x[2]]);
static int vecpre[maxdep][maxn];
auto &pre=vecpre[dep];
vector<arrr> nxtq;
static int hs[maxn*2*idx+idx+5];
for(int i=0,j;i<m;i=j+1){
j=i;while(j<m&&q[i][0]==q[j][0]) j++;j--;
static int us[maxn],uc;
uc=0;
for(int k=i;k<=j;k++){
int d=id(q[k]);
if(hs[d]==0){
us[++uc]=d;
bool flg=1;
int &u=now[q[k][2]];
//humanity intelligence.
if(u==11) flg=0;
else if(u==17||u==18){
if(q[k][0]!=mxv[0]||q[k][1]!=0)
u=11,flg=0;
}else if(u==22||u==13){
if(q[k][0]!=0||q[k][1]!=mxv[1])
u=11,flg=0;
}else if(u==15){
if(isa[0][q[k][0]])
u=11,flg=0;
else
u=15,flg=0;
debug_cnt_15++;
}else if(u==16){
if(isa[1][q[k][1]])
u=11,flg=0;
else
u=16,flg=0;
debug_cnt_16++;
}else if(u==24){
if(isa[0][q[k][0]]||isa[1][q[k][1]])
u=11,flg=0;
else
u=24,flg=0;
}else if(u==6){
if(isa[0][q[k][0]]||isa[1][q[k][1]])
u=11,flg=0;
}else if(u==3){
if(q[k][0]==mxv[0]&&!isa[0][q[k][0]])
u=3,flg=0;
}else if(u==5){
if(q[k][1]==mxv[1]&&!isa[1][q[k][1]])
u=5,flg=0;
}else if(u==7){
if(q[k][1]==0&&!isa[1][q[k][1]])
u=7,flg=0;
}else if(u==9){
if(q[k][0]==0&&!isa[0][q[k][0]])
u=9,flg=0;
}else if(u==19){
if(q[k][0]==0&&!isa[0][q[k][0]])
u=19,flg=0;
else if(q[k][1]==mxv[1]&&!isa[1][q[k][1]])
u=19,flg=0;
}
if(flg) nxtq.push_back(q[k]);
hs[d]=k;
}pre[k]=hs[d];
}for(int k=1;k<=uc;k++) hs[us[k]]=0;
}
for(int c:{0,1}) for(auto &x:prea) isa[c][x[c]]=0;
int mid=(n-1)>>1;
vector<arr> la,ra;
for(int i=0;i<=mid;i++) la.push_back(prea[i]);
for(int i=mid+1;i<n;i++) ra.push_back(prea[i]);
solve(dep+1,la,nxtq,qv,nxtq.size());solve(dep+1,ra,nxtq,qv,nxtq.size());
for(int i=0;i<m;i++) now[q[i][2]]=now[q[pre[i]][2]];
}
int n,m;
arr aa[maxn],b[maxn];
int main(){
freopen("cards.in","r",stdin);
freopen("cards.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&aa[i][0],&aa[i][1]);
for(int i=1;i<=m;i++) scanf("%d%d",&b[i][0],&b[i][1]);
for(int i=1;i<=m;i++) now[i]=1;
vector<arr> a,prea;
vector<arrr> q;
for(int i=1;i<=n;i++) a.push_back(aa[i]);
prea=a;
for(int i=1;i<=m;i++) q.push_back({b[i][0],b[i][1],i});
array<vector<int>,2> qv;
sort(q.begin(),q.end(),[&](arrr x,arrr y){return x[0]<y[0];});
for(int c:{0,1}){
for(auto &x:q) qv[c].push_back(x[c]);
sort(qv[c].begin(),qv[c].end());
}
int l[2]={(int)qv[0].size(),(int)qv[1].size()};
//cerr<<"ok1"<<endl;
for(int c:{0,1}){
unordered_map<int,int> h;
int tot=-1,lst=-1;
sort(a.begin(),a.end(),[&](arr x,arr y){return x[c]<y[c];});
//qv has been sorted.
int j=0;
for(int i=0;i<l[c];i++){
while(j<n&&a[j][c]<qv[c][i]){
++tot;
h[a[j][c]]=tot;lst=-1;j++;//??!!
}
if(lst==-1) ++tot;
h[qv[c][i]]=tot;lst=qv[c][i];
}while(j<n){
++tot;
h[a[j][c]]=tot;lst=-1;j++;
}
for(auto &x:prea) x[c]=h[x[c]];
for(auto &x:q) x[c]=h[x[c]];//!!!
for(auto &x:qv[c]) x=h[x];
l[c]=unique(qv[c].begin(),qv[c].end())-qv[c].begin();
qv[c].resize(l[c]);
}//cerr<<"ok2"<<endl;
vector<int> pre(q.size(),0);
vector<arrr> nxtq;
static int hs[maxn*2*idx+idx+5];
for(int i=0,j;i<m;i=j+1){
j=i;while(j<m&&q[i][0]==q[j][0]) j++;j--;
for(int k=i;k<=j;k++){
int d=id(q[k]);
if(hs[d]==0){
nxtq.push_back(q[k]);
hs[d]=k;
}pre[k]=hs[d];
}for(int k=i;k<=j;k++) hs[id(q[k])]=0;
}
solve(1,prea,nxtq,qv,nxtq.size());
for(int i=0;i<m;i++) now[q[i][2]]=now[q[pre[i]][2]];
bool first=true;
for(int i=1;i<=m;i++){
if(acc[now[i]]){
if(!first) printf(" ");
first=false;
printf("%d", i);
}
}
printf("\n");
cerr<<debug_cnt_n<<' '<<debug_cnt_q<<' '<<debug_cnt_qv<<endl;
cerr<<debug_cnt_15<<' '<<debug_cnt_16<<endl;
return 0;
}

浙公网安备 33010602011771号