[冲刺国赛2022] 模拟赛2
矩阵
题目描述
有一个 \(n\times n\) 的矩阵,初始时所有位置上都是 \(0\),每次你可以选择若干行 \(r_1,r_2...r_p\) 和若干列 \(c_1,c_2...c_q\),把这些行列交点处的位置都变成 \(1\),即把 \((r_i,c_j),1\leq i\leq p,1\leq j\leq q\) 这些位置上的数字都变成 \(1\)
\(n\leq 3000\),询问次数 \(\leq 26\)
解法
首先考虑简化问题,如果只是对角线上的元素为 \(0\) 怎么办?由于同行同列不能同时选,可以考虑二进制分组。对于每个二进制位,每次把为 \(0\) 的行和为 \(1\) 的列(还有为 \(1\) 的行和为 \(0\) 的列)拿出来做,这样一个点由于其横纵坐标不同,一定会在横纵坐标不同的那个二进制位上被染黑,询问次数 \(2\log n\)
本题就是多了对角线下面的那一列,尝试把上面的做法拓展下来。我们把列奇偶分组,把奇数列和所有的行做,把偶数列和所有的行做。比如在做奇数列的时候,我们可以把 \(1,2\) 压缩成一行,这样就转化到了上面的情况。
但是好像要花费 \(4\log n\) 的询问次数,其实对角线的问题是可以做到 \(1\log n\) 的,具体来说我们把行和列都重新编号。值域为 \([0,2^{13})\),我们只把有 \(6\) 个 \(1\) 的数取出来,给它们重编号,这样每次只用把为 \(0\) 的行和为 \(1\) 的列拿出来做。
这是因为重编号方式不存在数位的包含关系,所以一定存在行的某个数位是 \(0\),列的某个数位是 \(1\),因为 \({13\choose 6}>1500\),所以编号也是够用的,那么我们就把询问次数优化到了 \(2\log n\)
#include <cstdio>
#include <vector>
using namespace std;
const int M = 3005;
#define pb push_back
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,bit[1<<13],id[1<<13],a[M],b[M];
vector<int> A[M],B[M];
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
n=read();read();
for(int i=1;i<(1<<13);i++)
{
bit[i]=bit[i>>1]+(i&1);
if(bit[i]==6) id[++m]=i;
}
for(int i=1;i<=n;i+=2)
a[i]=b[i]=b[i+1]=id[(i+1)>>1];
for(int t=0;t<13;t++)
{
k++;
for(int i=1;i<=n;i++)
if(!(b[i]>>t&1)) A[k].pb(i);
for(int i=1;i<=n;i+=2)
if(a[i]>>t&1) B[k].pb(i);
if(A[k].empty() || B[k].empty())
A[k].clear(),B[k].clear(),k--;
}
//
for(int i=0;i<=n;i+=2)
a[i]=b[i]=b[i+1]=id[(i+2)>>1];
for(int t=0;t<13;t++)
{
k++;
for(int i=1;i<=n;i++)
if(!(b[i]>>t&1)) A[k].pb(i);
for(int i=2;i<=n;i+=2)
if(a[i]>>t&1) B[k].pb(i);
if(A[k].empty() || B[k].empty())
A[k].clear(),B[k].clear(),k--;
}
printf("%d\n",k);
for(int i=1;i<=k;i++)
{
printf("%d %d",A[i].size(),B[i].size());
for(int x:A[i]) printf(" %d",x);
for(int x:B[i]) printf(" %d",x);
puts("");
}
}
字符串
题目描述
给定一个 \(01\) 串 \(s\),记它长度为 \(i\) 的后缀为 \(s_i\),我们把总共 \(n\) 个 \(s_i\) 都插入到一棵字典树中,并且把每个 \(s_i\) 最后一个字符对应的节点涂黑,剩下的节点都是白色,不难发现总共会涂黑恰好 \(n\) 个节点。
之后,我们把所有度数不为 \(2\) 的节点都涂黑( 指把儿子个数不为 \(1\) 的节点都涂黑 ),特别的,也涂黑根节点。我们选出所有的黑色点对 \(i,j\)(需要满足 \(i\) 的深度不超过 \(j\) 的深度),且 \(i\) 到 \(j\) 的路径上不经过其它黑色点,把 \(i\) 到 \(j\) 路径上的字符连起来形成一个字符串,并且把这个字符串的本质不同子串数累加进答案。
\(n\leq 10^6\)
解法
有一个很妙的题目转化,考虑后缀自动机就是对每个前缀建立 \(\tt tire\) 树之后压缩得到的结果,那么我们对反串建出后缀自动机,答案就是 \(\tt parent\) 树上每条边所代表字符串的本质不同子串个数之和。
考虑它一定对应着原串的一个区间,所以可以暴力区间本质不同的子串个数,时间复杂度 \(O(n\log ^2n)\)
更好的做法是,通过后缀自动机的 转移
来构建一棵新的 \(\tt tire\) 树。\(v\) 的父亲就是 \(\tt parent\) 树上 \(\tt dfs\) 序最大的点 \(u\),满足存在转移 \(ch[u][c]=v\),要求 \(\tt dfs\) 序最大是因为这样 \(u,v\) 之间就只差一个字符。
自底向上看,这棵 \(\tt tire\) 树是后代逐渐剔除最后一个字符以得到祖先的结构,那么自顶向下看,\(\tt parent\) 树上节点所代表的字符串一定对应着 \(\tt tire\) 树上祖先到根的链所对应的字符串,以长度就可以区分是哪个祖先。
那么问题就只剩下了计算 \(\tt tire\) 树上每个点到根链所对应字符串的本质不同的子串个数,记为 \(dp[u]\),考虑 \(dp[u]\) 和 \(dp[fa]\) 的区别就是多出来了一些后缀。但是这些后缀也不是全部都能贡献,有些在祖先出现过的无法贡献。可以考虑求出祖先和点 \(u\) 在后缀自动机上的最深的 \(lca\),记为 \(x\),就可以得到:
至于这个 \(x\) 可以找到 \(\tt dfs\) 序的前驱和后继,然后求两次 \(\tt lca\) 即可,时间复杂度 \(O(n\log n)\)
但是这种做法跑得比 \(O(n\log^2n)\) 的做法还慢,需要树链剖分求 \(\tt lca\) 和 \(\tt zkw\) 线段树求前驱后继才可以通过。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 2000005;
#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,last,cnt,pr[M],st[M];char s[M];
int m,p,dep[M],dfn[M],f[M<<2],id[M];ll ans,dp[M];
int fa[M],siz[M],son[M],top[M];
struct node{int len,fa,ch[2];}a[M];vector<int> g[M];
//part I : build sam and target tire
void add(int c)
{
int p=last,np=last=++cnt;
a[np].len=a[p].len+1;
for(;p && !a[p].ch[c];p=a[p].fa) a[p].ch[c]=np;
if(!p) a[np].fa=1;
else
{
int q=a[p].ch[c];
if(a[q].len==a[p].len+1) a[np].fa=q;
else
{
int nq=++cnt;
a[nq]=a[q];a[nq].len=a[p].len+1;
a[q].fa=a[np].fa=nq;
for(;p && a[p].ch[c]==q;p=a[p].fa)
a[p].ch[c]=nq;
}
}
}
void dfs(int u)
{
dep[u]=dep[fa[u]=a[u].fa]+1;
dfn[u]=++m;id[m]=u;siz[u]=1;
for(int i=0;i<2;i++) if(a[u].ch[i])
pr[a[u].ch[i]]=u;
for(int v:g[u])
{
dfs(v);siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(int u,int tp)
{
top[u]=tp;
if(son[u]) dfs2(son[u],tp);
for(int v:g[u])
if(v^son[u] && v^fa[u]) dfs2(v,v);
}
void build()
{
for(int i=2;i<=cnt;i++)
g[a[i].fa].push_back(i);
dfs(1);dfs2(1,1);
for(int i=1;i<=cnt;i++) g[i].clear();
for(int i=2;i<=cnt;i++)
g[pr[i]].push_back(i);
}
//part II : zkw to maintain pre and suf
void upd(int x,int d)
{
for(f[x+p]=d,x=(x+p)>>1;x;x>>=1)
f[x]=f[x<<1]|f[x<<1|1];
}
int pre(int x)
{
for(x=p+x;x>1;x>>=1)
if((x&1) && f[x^1]) {x^=1;break;}
if(x<2) return 0;
for(;x<p;x<<=1,x^=f[x^1]);
return x-p;
}
int suf(int x)
{
for(x=p+x;x>1;x>>=1)
if(!(x&1) && f[x^1]) {x^=1;break;}
if(x<2) return 0;
for(;x<p;x<<=1,x^=f[x]^1);
return x-p;
}
//part III : calc the answer
int lca(int u,int v)
{
if(!u || !v) return 0;
while(top[u]^top[v])
{
if(dep[top[u]]<=dep[top[v]]) swap(u,v);
u=fa[top[u]];
}
u=dep[u]<dep[v]?u:v;
return a[u].len;
}
void solve(int u)
{
st[a[u].len]=u;int x=dfn[u];
int l=max(lca(u,id[pre(x)]),lca(u,id[suf(x)]));
dp[u]=a[u].len-l+dp[pr[u]];
upd(x,1);
for(int v:g[u]) solve(v);
upd(x,0);
ans+=dp[st[a[u].len-a[a[u].fa].len]];
}
signed main()
{
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
scanf("%s",s+1);n=strlen(s+1);
reverse(s+1,s+1+n);last=cnt=1;
for(int i=1;i<=n;i++) add(s[i]-'0');
build();
for(p=1;p<cnt+2;p<<=1);
solve(1);
printf("%lld\n",ans);
}