朝花夕拾
Day1
学字符串哈希,逆天的是,我现在才知道字符串哈希的小下标是高位,大下标是低位,我一直都是反过来的,难怪感觉这么奇怪不用求逆元,实际上字符串哈希就是一个 \(\mathrm{base}\) 进制的数,不需要反转过来,然后知道了二分+哈希可以做 \(\mathrm{lcp}\),我也忘了还有什么可以求 \(\mathrm{lcp}\) 了。
例题:月光
Statement
给定两个字符串 \(s,t\),你需要将 \(t\) 划分成若干段,然后要求每一段都是 \(s\) 的后缀,求最小划分段数,不合法输出 \(-1\)。时间复杂度 \(O(n\log(n))\)。
Solution
首先很容易设计出划分数 \(\mathrm{dp}\),\(f_i=f_j+1,t[j+1,i]=s[1,i-j]\)。第一眼看上去像等差数列,确实可以这么做,但是还有一个比较巧的办法。我们稍微改变一下形式,让 \(j\) 贡献到 \(i\),这样的好处就是 \(j\) 能贡献到的位置是一段区间,容易发现区间的长度是 \(t[j,m]\) 和 \(s\) 的 \(\mathrm{lcp}\),然后感觉就像单点查然后区间取 \(\min\),要用 \(\mathrm{segbeat}\),仔细想了一下,实际上可以改成前缀取 \(\min\),因为前面的值我们不会在后面查询,这样就可以用类似差分的思想直接把修改挂在区间右端点上,直接树状数组查后缀 \(\min\) 就做完了。
Code
#include<bits/stdc++.h>
using ull=unsigned long long;
using namespace std;
namespace IO{
#define gc getchar
#define pc putchar
#define ps puts
inline int in(){
int x=0,f=1;
char c=gc();
for(;!isdigit(c);c=gc())if(c=='-')f=-1;
for(;isdigit(c);c=gc())x=x*10+(c-'0');
return x*f;
}
inline void out(int x){
if(x<0)pc('-'),x=-x;
if(x>=10)out(x/10);
pc(x%10+'0');
}
inline void outsp(int x){out(x),pc(' ');}
inline void outln(int x){out(x),ps("");}
}
using namespace IO;
const int N=5e5+5,B=131;
int n,m,f[N];
ull h[2][N],pw[N];
char s[N],t[N];
ull calc(bool p,int l,int r){return h[p][r]-h[p][l-1]*pw[r-l+1];}
void add(int x,int y){for(;x>=1;x-=x&-x)f[x]=min(f[x],y);}
int ask(int x){if(!x)return 0;int y=N;for(;x<=m;x+=x&-x)y=min(y,f[x]);return y;}
int main(){
scanf("%s%s",s+1,t+1),n=strlen(s+1),m=strlen(t+1);
pw[0]=1;
for(int i=1;i<=max(n,m);i++)pw[i]=pw[i-1]*B;
for(int i=1;i<=n;i++)h[0][i]=h[0][i-1]*B+(s[i]-'a');
for(int i=1;i<=m;i++)h[1][i]=h[1][i-1]*B+(t[i]-'a');
for(int i=1;i<=m;i++)f[i]=N;
for(int i=0;i<m;i++){
int x=ask(i);
if(x>=N)continue;
int L=0,R=min(n,m-i);
while(L<R){
int M=(L+R+1)>>1;
if(calc(1,i+1,i+M)==calc(0,1,M))L=M;
else R=M-1;
}
if(L)add(i+L,x+1);
}
int x=ask(m);
out(x<N?x:-1);
return 0;
}
Day2
什么叫点双里面可以有割点啊,原来根本不知道,然后把 \(\mathrm{tarjan}\) 的模板题全部过了一边。发现有些例题我都不会啊啊啊。
例题:矿场搭建
Statement
给你一个无向图,你要安排最少的安全出口并输出方案数,保证这个图无论删掉哪一个点,全图的点都能到达安全出口。
Solution
首先考虑求出割点和点双。然后我们在每个点双之间分析。注意到,一个点双的割点数量等于这个点双到外部的通路数量,所以说我们可以分类讨论出点双里面的割点数。如果没有割点,那么说明和外界没有连边,那么这个点双里面至少要有两个安全出口,不然炸掉其中一个点双里面就没有出口了。如果有一个割点,那么这个点双内要有一个非割点是安全出口,因为如果割点没炸,可以通过割点去其他点双的出口,如果炸了,只能在点双里面找出口了。如果有至少两个割点,那么无论炸掉哪个割点,我都可以到达外界,所以不需要建安全出口。方案数也是好求的。
Code
#include<bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
namespace IO{
#define gc getchar
#define pc putchar
#define ps puts
inline int in(){
int x=0,f=1;
char c=gc();
for(;!isdigit(c);c=gc())if(c=='-')f=-1;
for(;isdigit(c);c=gc())x=x*10+(c-'0');
return x*f;
}
inline void out(int x){
if(x<0)pc('-'),x=-x;
if(x>=10)out(x/10);
pc(x%10+'0');
}
inline void outsp(int x){out(x),pc(' ');}
inline void outln(int x){out(x),ps("");}
}
using namespace IO;
const int N=505;
int n,m,k,tot,dfn[N],low[N];
bool cut[N];
stack<int> s;
vector<int> g[N],h[N];
void dfs(int u,int f){
dfn[u]=low[u]=++tot,s.push(u);
int c=0;
for(int v:g[u])if(v!=f){
if(!dfn[v]){
dfs(v,u),c++,low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
if(f)cut[u]=1;
k++;
while(s.top()!=v)h[k].pb(s.top()),s.pop();
h[k].pb(v),s.pop(),h[k].pb(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
if(!f&&c>1)cut[u]=1;
if(!f&&!c)h[++k].pb(u);
}
signed main(){
for(int t=1;;t++){
m=in();
if(!m)break;
for(int i=1;i<=m;i++){
int u=in(),v=in();
g[u].pb(v),g[v].pb(u);
n=max({n,u,v});
}
dfs(1,0);
int ans1=0,ans2=1;
for(int i=1;i<=k;i++){
int cnt=0,c=h[i].size();
for(int u:h[i])cnt+=cut[u];
if(cnt==0)ans1+=2,ans2*=c*(c-1)/2;
if(cnt==1)ans1++,ans2*=c-1;
h[i].clear();
}
printf("Case %lld: %lld %lld\n",t,ans1,ans2);
while(!s.empty())s.pop();
for(int i=1;i<=n;i++)dfn[i]=low[i]=cut[i]=0,g[i].clear();
n=k=tot=0;
}
return 0;
}
Day3
学树剖,怎么代码这么长啊服了,板子题调了好久,最后还是汪总发现函数传参反了,才知道,我又犯了经常犯的错:输入直接传参,然后写树剖板子的时候发现树剖 \(\mathrm{LCA}\) 也可以用 \(\mathrm{dfn}\) 维护,不用记录深度。
浙公网安备 33010602011771号