Loading

同余最短路学习笔记

同余最短路通常可以解决形如给出若干转移(例如加某个数、乘某个数),用它们拼出大整数而产生的问题。

其工作原理是选择一个模数 \(m\),将整数分成 \(m\) 个同余类,将每个同余类看做一个状态。那么拼 \(x\) 的过程就相当于添一条路 \(i\xrightarrow{x}(i+x)\bmod m\)。然后根据每个状态的答案计算总答案。

P3403 跳楼机

为了减少状态,可以取 \(m=\min\{x,y,z\}\),被取到的值就不必建边了。建边后,从 \(1\) 开始跑最短路,得出每一个同余类中,最小的能到达的位置。设余数为 \(i\) 的同余类的最小位置为 \(d_i\),显然 \(d_i+km\) 也都能到达。所以答案为

\[\sum_{i=0}^{m-1}[h\ge d_i]\left(\left\lfloor\frac{h-d_i}{m}\right\rfloor+1\right) \]

代码太丑就不放了。有一些细节:\(m\) 可能为 \(1\),所以要对 \(d_{1\bmod m}\) 赋初始值,并且最短路要开 long longP2371 [国家集训队]墨墨的等式 和这道题几乎完全一样。

ARC084B Small Multiple

题目大意

给定一个正整数 \(2\le K\le 10^5\),求它的所有正整数倍数中,十进制下数位和的最小值。

题解

由于每个数都可以通过 \(+1\)\(\times 10\) 操作得到,并且其权值就是 \(+1\) 的操作数,所以直接把 \(K\) 当作模数,每个点添加两个转移,然后 01-BFS 即可。

#include<bits/stdc++.h>
using namespace std;
constexpr int N=1e5+5;
int n,d[N];
int main(){
	cin>>n;
	memset(d,0x3f,sizeof(d));
	deque<int>q;
	q.push_front(1);d[1]=1;
	while(q.size()){
		int u=q.front(),v;q.pop_front();
		v=(u*10)%n;
		if(d[u]<d[v]){
			d[v]=d[u];
			q.push_front(v);
		}
		v=(u+1)%n;
		if(d[u]+1<d[v]){
			d[v]=d[u]+1;
			q.push_back(v);
		}
	}
	printf("%d\n",d[0]);
	return 0;
}

P4156 [WC2016]论战捆竹竿

题目大意

给定一个长为 \(n\) 的字符串 \(s\),其 公共前后缀(包含空串和整串) 从短到长分别为 \(b_1,b_2,\cdots,b_k\)。记序列 \(a=\{n-|b_i|\}\)。求在 \([0,w]\) 中有多少个数能表示成 \(n+\sum_{i=1}^kx_ia_i\),其中 \(x_i\) 为自然数。

定理

一个字符串 \(S\) 的 border 的长度有序排列成的序列能够被划分成 \(O(\log |S|)\) 个等差数列。

证明

我们知道,如果 \(s\) 有长为 \(x\)\(y\) 的周期且 \(x+y-\gcd(x,y)\le |s|\),那么 \(\gcd(x,y)\) 也是 \(s\) 的周期。具体证明可以用数学归纳法。

\(n-p,n-q\) 分别为 \(s\) 的两个长度不小于 \(s\) 一半的 border 的长度,这样一来,\(s\) 长为 \(p\) 的前缀是 \(s\) 的周期,\(s\) 长为 \(\gcd(p,q)\) 的前缀也是周期。

假设 \(n-p\)\(s\) 最长 border 的长度,那么 \(p\mid q\)。于是就发现了一个公差为 \(-p\) 的等差数列:\(s\) 长为 \(n-p,n-2p,\cdots\) 的前缀都是 border。所以,所有长度大于等于一半的 border 的长度构成了等差数列。

我们知道 border 的 border 还是 border,所以对于长度小于一半的 border,可以递归应用这个性质,总共拆成 \(O(\log n)\) 个等差数列。

题解

尝试依次计算每个等差数列的贡献。

按照同余最短路的思想,记录一个数组 \(d\),其中 \(d_i\) 表示在 \(\bmod m\) 意义下余数为 \(i\) 的最小能到达的值。

考虑到等差数列 \(a,a+b,a+2b,\cdots,a+kb\) 时,先将 \(m\) 变成 \(a\)。这时要变化 \(d\) 数组。设新的 \(d\) 数组为 \(D\),则 \(D_i=\min_{d_j\equiv i\pmod a}d_j\)

然而这样显然不能得到所有的值。我们知道若 \(d_i\) 可达,则 \(d_i+km\) 可达,所以再建出边 \(i\xrightarrow{m}(i+m)\bmod a\),跑同余最短路。这些边形成了 \(\gcd(a,m)\) 个环,所以我们有更好的方法:将每个环单独考虑,只有环内的节点能够互相影响。在环内找到 \(d\) 最小的点,往后更新即可。

然后就是在这上面用等差数列更新。模数被改成了 \(a\),所以只要找到其中 \(b\) 形成的环,对于每个环找到最小的 \(d_i\),然后更新整个环就行了。因为 \(b\) 有个数的限制,所以要用单调队列。

我在 UOJ 上卡了半天常数都没有过 Extra Test,然后去借鉴了 UOJ 上和我码风相似的代码,发现我统计等差数列时可能把同一个数统计在两个等差数列里,最多会有两倍常数。修改之后就过了。

最终代码如下。时间复杂度为 \(O(n\log n)\)

代码

#pragma GCC optimize("Ofast,inline")
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int N=5e5+5;
constexpr ll INF=0x3f3f3f3f3f3f3f3f;
int n,m,nxt[N],bor[N],q[N],hd,tl,g[N],Sz;
ll w,dis[N],D[N],val[N];char s[N];
inline int Mod(const int&x){return x<m?x:x-m;}
inline void chkmin(ll&x,const ll&y){if(x>y)x=y;}
inline int prepare(int x){
	int ret=gcd(x,m),p=0;Sz=m/ret;x%=m;g[p++]=0;
	for(int u=x;u;u=Mod(u+x))g[p++]=u;
	return ret;
}
inline void getRing(){
	int k=0;for(int i=0;i<Sz;i++){
		g[i]++;if(g[i]==m)g[i]=0;
		if(dis[g[i]]<dis[g[k]])k=i;
	}
	rotate(g,g+k,g+Sz);
}
void updMod(int a){
	memset(D,0x3f,sizeof(ll[a]));
	for(int i=0;i<m;i++)chkmin(D[dis[i]%a],dis[i]);
	memcpy(dis,D,sizeof(ll[a]));
	int lst=m;m=a;for(int T=prepare(lst);;){
		for(int i=1;i<Sz;i++)chkmin(dis[g[i]],dis[g[i-1]]+lst);
		if(--T)getRing();else break;
	}
}
void add(int a,int d,int siz){
	if(m!=a)updMod(a);
	if(d<0)return;
	for(int T=prepare(d);;){
		ll delta=d;val[q[hd=tl=1]=0]=dis[g[0]]+a;
		for(int p=1,i;p<Sz;p++,delta+=d){
			while(hd<=tl&&p-siz>q[hd])hd++;
			i=g[p];chkmin(dis[i],val[q[hd]]+delta);
			while(hd<=tl&&val[q[tl]]>=dis[i]+a-delta)tl--;
			val[q[++tl]=p]=dis[i]+a-delta;
		}
		if(--T)getRing();else break;
	}
}
void ct(){
	scanf("%d%lld%s",&n,&w,s+1);w-=n;
	nxt[0]=-1;for(int i=1,j=-1;i<=n;i++){
		while(~j&&s[i]!=s[j+1])j=nxt[j];
		nxt[i]=++j;
	}
	memset(dis,0x3f,sizeof(ll[n]));dis[0]=0;int tot=0;
	for(int i=nxt[n];i;i=nxt[i])bor[tot++]=n-i;
	reverse(bor,bor+tot);m=n;
	for(int i=0,j;i<tot;i++){
		if(i==tot-1)updMod(bor[i]);else{
			for(j=i+1;j+1<tot&&bor[j]-bor[j-1]==bor[j+1]-bor[j];j++);
			add(bor[j],bor[j-1]-bor[j],j-i);
			i=j;
		}
	}
	ll ans=0;
	for(int i=0;i<m;i++)if(w>=dis[i])ans+=(w-dis[i])/m+1;
	printf("%lld\n",ans);
}
int main(){
	int T;scanf("%d",&T);while(T--)ct();
	return 0;
}
posted @ 2023-01-14 21:41  hihihi198  阅读(92)  评论(0)    收藏  举报