[冲刺国赛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\),就可以得到:

\[dp[u]=dp[fa]+len[u]-len[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);
}
posted @ 2022-05-27 16:31  C202044zxy  阅读(149)  评论(0编辑  收藏  举报