总结
怎么说呢?又被 ( t OneInDark) 按在地上摩擦。
考试其实是一个平时训练的投影,为什么 ( t T3) 我明明做过但是却从一开始就走了错误的方向?
我以前以为信竞的提升应该是靠做过题的经验,积累一些套路,所以在看到类似的题之后能做起,我称之为经验主义。但是今天的 ( t T3) 就是一个很好的例子,它的题目条件是不相交的真子串,但是因为我没有仔细分析,而是凭借经验就直接上了在后缀自动机上 (dp),但是这个方法只能做子串的情况,所以爆零。
( t T3) 明明可以通过分析得出来在原串上 (dp) 的做法,再结合一些性质考虑就可以通过。
所以我想要打破之前的观念,今天我所提出的是逻辑主义,即做题在于掌握这道题特定的逻辑,然后再将这些逻辑应用到考试上去。经验主义的弊端在于很多时候细微的差别就决定了这道题能不能用这个做法,而凭感觉在强调逻辑的信息竞赛中是致命的,最近几次考试我很是深切地感受到了经验主义对我的负面影响。
反应到平时训练时,应该怎么做?我觉得应该贯通一道题的逻辑,每个步骤应该做到 它应该就是这样的
而不是 它好像是这样的
,那就要求所有的细节都要深究!
没关系,有问题就要改嘛,以后的路还很长^_^
人生
题目描述
一个 (n) 个点的有向图,每个点有颜色,部分点的颜色已经确定,定义一条任意相邻点不同色的路径为交错路径。我们要为所有未确定颜色的点定颜色,并且确定对于 (i<j) 的有向边 ((i,j)) 是否存在。
求有多少种方案使得交错路径的条数为奇数,对 (998244353) 取模。
(1leq nleq 2cdot 10^5)
解法
淦,考试时候脑子不清醒,一定要注意答案只和奇偶性有关
我们考虑在每个点为终点的时候统计一遍对路径条数的贡献,由于边是小连大所以当考虑 (i) 的终止路径时,可以只考虑前 (i) 个点。设 (dp[i][j][k][h]) 表示前 (i) 个点有 (j) 的白点和 (k) 个黑点终止路径条数为奇数(称之为奇点),路径总和的奇偶性为 (h),转移就只用考虑和奇点的连边:
- 当这个点定为偶白点:(dp[i+1][j][k][h]leftarrow dp[i][j][k][h]cdot c(k,1)cdot 2^{i-k})
- 当这个点定为奇白点:(dp[i+1][j+1][k][hoplus 1]leftarrow dp[i][j][k][h]cdot c(k,0)cdot 2^{i-k})
- 当这个点定为偶白点:(dp[i+1][j][k][h]leftarrow dp[i][j][k][h]cdot c(j,1)cdot 2^{i-j})
- 当这个点定为奇白点:(dp[i+1][j][k+1][hoplus 1]leftarrow dp[i][j][k][h]cdot c(j,0)cdot 2^{i-j})
其中 (c(x,y)) 表示从大小为 (x) 的集合中选出奇偶性为 (y) 的点的方案数,不难发现当 (x>0) 时 (c(x,y)=2^{x-1}),当 (x=0) 时,(c(x,0)=1,c(x,1)=0)
把 (c(x,y)) 带进我们的转移中就可以发现 (j,k) 都被消掉了,那么转移就只和 (j,k) 是否为 (0) 有关,重新定义一个更简单的状态,设 (dp[i][0/1][0/1][h]) 表示考虑了前 (i) 个点,有没有奇白点,有没有奇黑点,现在路径总和的奇偶性为 (h) 的方案数,转移就魔改一下暴力 (dp) 的方法即可,时间复杂度 (O(n))
#include <cstdio>
const int M = 200005;
const int MOD = 998244353;
#define int 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,ans,a[M],pw[M],dp[M][2][2][2];
signed main()
{
freopen("life.in","r",stdin);
freopen("life.out","w",stdout);
n=read();
for(int i=1;i<=n;i++)
a[i]=read();
pw[0]=1;
for(int i=1;i<=n;i++)
pw[i]=pw[i-1]*2%MOD;
if(a[1]!=0) dp[1][0][1][1]++;
if(a[1]!=1) dp[1][1][0][1]++;
for(int i=1;i<n;i++)
{
//第i个点选为黑色,考虑前面的白边
if(a[i+1]!=0)
{
for(int k=0;k<=1;k++)
for(int l=0;l<=1;l++)
{
//终止路径为偶数
dp[i+1][0][k][l]=(dp[i+1][0][k][l]+dp[i][0][k][l]*0)%MOD;
dp[i+1][1][k][l]=(dp[i+1][1][k][l]+dp[i][1][k][l]*pw[i-1])%MOD;
//终止路径为奇数
dp[i+1][0][1][l^1]=(dp[i+1][0][1][l^1]+dp[i][0][k][l]*pw[i])%MOD;
dp[i+1][1][1][l^1]=(dp[i+1][1][1][l^1]+dp[i][1][k][l]*pw[i-1])%MOD;
}
}
//第i个点选白色,考虑前面的黑边
if(a[i+1]!=1)
{
for(int j=0;j<=1;j++)
for(int l=0;l<=1;l++)
{
//终止路径为偶数
dp[i+1][j][0][l]=(dp[i+1][j][0][l]+dp[i][j][0][l]*0)%MOD;
dp[i+1][j][1][l]=(dp[i+1][j][1][l]+dp[i][j][1][l]*pw[i-1])%MOD;
//终止路径为奇数
dp[i+1][1][0][l^1]=(dp[i+1][1][0][l^1]+dp[i][j][0][l]*pw[i])%MOD;
dp[i+1][1][1][l^1]=(dp[i+1][1][1][l^1]+dp[i][j][1][l]*pw[i-1])%MOD;
}
}
}
for(int j=0;j<=1;j++)
for(int k=0;k<=1;k++)
ans=(ans+dp[n][j][k][1])%MOD;
printf("%lld
",ans);
}
赢家
题目描述
给定一个 (n) 个点 (m) 条边的有向图,求给每一条边定向使得 (1) 和 (2) 能到达同一个点的方案数。
(1leq nleq 15,1leq mleqfrac{n(n-1)}{2})
解法
考虑用总数 (2^m) 减去不合法的方案数得到答案。然后我们再考虑不合法的方案数长什么样子,设 (1) 能到达的点集为 (S),(2) 能到达的点集为 (T),也就是求 (S) 和 (T) 交集为空的方案数。
这一步的依据是什么呢?( t tly) 巨佬告诉我们:一个是存在可以到达,一个是所有都不能到达,存在和所有之间我们肯定选所有(因为限制更强烈),那么正难则反的转化可以做到这一步。
设 (f(S)) 为只考虑 (S) 的导出子图,(1) 可以到达 (S) 的边定向方案数,(g(S)) 的定义类似。那么枚举 (S) 和 (T),要求 (S) 和 (T) 之间没有边,其他的部分到 (S,T) 的边必须是指向 (S,T) 的,内部的边可以自由定向,设导出子图 (S) 内部随便定边的方案数是 (p(S)),那么答案为:
那么计算答案的时间复杂度就是子集枚举的 (O(3^n)),现在考虑怎么计算 (f(S))((g(T)) 类似)
直接转移是很难的,因为后面还可能影响到前面。但是算不能到达的方案数可能更容易,那么我们还是正难则反,用总数减去有些点不能到达的方案数。枚举可以到达的集合 (T)(是 (S) 的真子集),(S-T) 到 (T) 的边只能指向 (T),内部的边可以乱定,减去的方案数是:
这个也是子集枚举,时间复杂度 (O(3^n))
#include <cstdio>
const int M = 1005;
const int MOD = 1e9+7;
#define int 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,m,ans,a[M],b[M],pw[M],vis[M],f[1<<15],g[1<<15],p[1<<15];
signed main()
{
freopen("winner.in","r",stdin);
freopen("winner.out","w",stdout);
n=read();m=read();read();pw[0]=1;
for(int i=1;i<=m;i++)
{
a[i]=read(),b[i]=read();
pw[i]=pw[i-1]*2%MOD;
}
//计算导出子图的2^边数
for(int i=0;i<(1<<n);i++)
{
for(int j=1;j<=m;j++)
if((i&(1<<a[j]-1)) && (i&(1<<b[j]-1)))
p[i]++;
p[i]=pw[p[i]];
}
//求f[S]
for(int s=0;s<(1<<n);s++)
{
if(!(s&1)) continue;//必须要包含1
f[s]=p[s];
for(int t=s;;t=(t-1)&s)
{
if(s!=t) f[s]=(f[s]-f[t]*p[s-t])%MOD;
if(t==0) break;
}
}
//求g[s]
for(int s=0;s<(1<<n);s++)
{
if(!(s&2)) continue;//必须要包含2
g[s]=p[s];
for(int t=s;;t=(t-1)&s)
{
if(s!=t) g[s]=(g[s]-g[t]*p[s-t])%MOD;
if(t==0) break;
}
}
int all=(1<<n)-1;
for(int i=0;i<(1<<n);i++)
{
int s=all-i;
//注意不能有边跨越
for(int j=1;j<=n;j++)
{
vis[j]=0;
if(i&(1<<j-1)) vis[j]=1;
}
for(int j=1;j<=m;j++)
{
if(!vis[a[j]] && (i&(1<<b[j]-1)))
vis[a[j]]=1,s-=(1<<a[j]-1);
if(!vis[b[j]] && (i&(1<<a[j]-1)))
vis[b[j]]=1,s-=(1<<b[j]-1);
}
for(int j=s;;j=(j-1)&s)
{
ans=(ans+f[i]*g[j]%MOD*p[all-i-j])%MOD;
if(j==0) break;
}
}
ans=pw[m]-ans;
printf("%lld
",(ans+MOD)%MOD);
}
黑红兔
题目描述
解法
我草你妈,正解这么几把简单老子还想不到,我是傻逼
首先把串反过来,那么问题变成了前一个是后一个的子串。
直接在后缀自动机上面 (dp) 是不行的(不满足后缀的关系),那么我们考虑在原串上 (dp),设 (dp[i]) 表示考虑到 (i) 的最大的 (k),首先我们要考察一些性质再开始做。
性质 (1):答案的形式一定是 (1,2,3...k),而且后一个串一定是前一个串接上单个字符的形式。
性质 (2):(dp[i]leq dp[i-1]+1,dp[i]geq dp[i-1]),这个柿子和求 ( t height) 那个东西很像
利用性质 (2),我们可以每次让 (dp[i]=dp[i-1]+1),然后暴力减少它直到合法,不难分析出减少次数是 (O(n)) 的。那么问题转化成怎么判断某个 (dp) 值合法,我们分别去掉子串 ([i-dp[i]+1,i]) 的首字符和尾字符,然后去查新子串在前面出现位置的最大 (dp) 值,设其为 (x),那么如果 (x+1geq dp[i]) 就说明他是合法的。
现在问题变成了怎么找这个最大的 (dp) 值,我们把原串在后缀自动机上定位,可以预处理加倍增套路地 (O(log n)) 做到。其实可以查找他子树内每一个前缀的 (dp) 值,就可以用 (dfn) 序加树状数组解决子树最大值了。为了保证选出来的子串不相交这个条件,我们维护一个指针 (j),在 (dp[i]) 减小的时候移动并加入对应的前缀即可,时间复杂度 (O(nlog n))
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 1000005;
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,cnt,last,tot,f[M],pos[M],fa[M][20];
int ans,Ind,dp[M],in[M],out[M],mx[4*M];char s[M];
struct node
{
int len,fa,ch[26];
}a[M];
struct edge
{
int v,next;
edge(int V=0,int N=0) : v(V) , next(N) {}
}e[M];
void add(int c)
{
int p=last,np=last=++cnt;
a[np].len=a[p].len+1;
pos[a[np].len]=np;
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)
{
in[u]=++Ind;
for(int i=1;i<20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==fa[u][0]) continue;
fa[v][0]=u;
dfs(v);
}
out[u]=Ind;
}
int get(int l,int r)//找到子串[l,r]的位置
{
int p=pos[r];
for(int i=19;i>=0;i--)
if(a[fa[p][i]].len>=r-l+1)
p=fa[p][i];
return p;
}
void add(int i,int l,int r,int x,int f)
{
if(l==r)
{
mx[i]=max(mx[i],f);
return ;
}
int mid=(l+r)>>1;
if(mid>=x) add(i<<1,l,mid,x,f);
else add(i<<1|1,mid+1,r,x,f);
mx[i]=max(mx[i<<1],mx[i<<1|1]);
}
int ask(int i,int l,int r,int L,int R)
{
if(L<=l && r<=R) return mx[i];
if(L>r || l>R) return 0;
int mid=(l+r)>>1;
return max(ask(i<<1,l,mid,L,R),ask(i<<1|1,mid+1,r,L,R));
}
int chk(int x,int len)
{
if(len==1) return 1;
int p1=get(x-len+2,x),p2=get(x-len+1,x-1);
return max(ask(1,1,cnt,in[p1],out[p1]),ask(1,1,cnt,in[p2],out[p2]))>=len;
}
int main()
{
//freopen("brr.in","r",stdin);
//freopen("brr.out","w",stdout);
n=read();scanf("%s",s+1);
last=cnt=1;
//建反串的后缀自动机
for(int i=n;i>=1;i--) add(s[i]-'a');
for(int i=2;i<=cnt;i++)
{
int j=a[i].fa;
e[++tot]=edge(i,f[j]),f[j]=tot;
}
dfs(1);
for(int i=1,j=0;i<=n;i++)
{
dp[i]=dp[i-1]+1;
while(!chk(i,dp[i]))
{
dp[i]--;j++;
add(1,1,cnt,in[pos[j]],dp[j]+1);
}
ans=max(ans,dp[i]);
}//bcbb
printf("%d
",ans);
}