【BZOJ2118】墨墨的等式【循环节做法】
取最小的ai,记作m。如果x能被凑出来,那么x+m就也能被凑出来。
根据这个思路,取数组$DP(0..m-1)$,表示能构成的最小数,使得$DP(i)\%m=0$,使用的是除开m的其他数
那么dp转移就是:$DP[(x+v)\%m] \leftarrow DP(x)+v$,很像最短路的松弛操作
所以我们把0..m-1看成图节点,建边后跑最短路即可
大多数做法就直接跑spfa了,其实有更好的方法
对于一个数v,考虑它贡献的边
- 从点 x出发, 有边
- (x+v, x)
- 从点 x+v出发, 有边
- (x+2v, x+v)
- 从点 x+2v出发, 有边
- (x+3v, x+v)
事实上,根据数论我们知道, (x + kv) % m是一个循环节,只会遍经m / gcd(m,v)个点。这些点看作一个环的话,共有gcd(M,v)个环
如果沿着环上的边进行转移,效率能提高不少。然而这样是可以的,也就是把边集划分后,分别进行最短路算法(dijkstra)。
Let h = gcd(M,v) for i = 0 to h-1 Let S = {} for j = 0 to M/h S union ( (i + v*j) % M, (i + v*(j+1)) % M ) /* notation: ( tail, head ) */ Find the vertex y with shortest distance from 0 from the set of vertices in the cycle S consider edges e in cycle S, starting from vertex y relax( DP(e.head), DP(e.tail) + v )
上面步骤的复杂度是 O(M),因此总的复杂度是 O(M*N)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=12+3; const int M=5e5+3; typedef long long ll; const ll INF=0x3f3f3f3f3f3f3f3f; int n,m,a[N]; ll d[M]; ll solve(ll x) { ll ret=0; for(int i=0;i<m;i++) if(d[i]<=x) ret+=(x-d[i])/m+1; return ret; } ll Bmn,Bmx; int gcd(int a,int b) { return b==0?a:gcd(b,a%b); } int main() { scanf("%d%lld%lld",&n,&Bmn,&Bmx); for(int i=1;i<=n;i++) scanf("%d",&a[i]); sort(a+1,a+1+n); int t=1; while(a[t]==0)t++; m=a[t]; memset(d,0x3f,sizeof d); d[0]=0; for(t++;t<=n;t++) { int v=a[t]; int h=gcd(m,v); for(int i=0;i<h;i++) { int now=i,st=-1; while(1) { if(st==-1 || d[st]>d[now]) st=now; now=(now+v)%m; if(now==i)break; } if(st==-1 || d[st]==INF)continue; now=st; ll val=d[st]; do { d[now]= val= min(val,d[now]); now=(now+v)%m; val+=v; }while(now!=st); } } printf("%lld",solve(Bmx)-solve(Bmn-1)); return 0; }
然而,实际运行时,玄学的spfa竟然还快一些