zoukankan      html  css  js  c++  java
  • 同余最短路

    同余最短路

    同余最短路是一种最短路的应用 ,一般用来解决如下问题:

    数列 ({a_n}) 已知,求 (n) 元多项式 (sumlimits_{i=1}^na_ix_i=k) 的最值之类的问题。

    我们通过一道题来引入:

    引:跳楼机

    (link:luogu P3403)

    形式化的题意:给定四正整数 (x,y,z,h),求有多少个正整数 (y) 满足 ((y<h)land (y=ax+by+cz+1)) 对于 (a,b,c) 有非负整数解。

    输入格式

    第一行一个整数 (h)

    第二行三个正整数,分别表示题目中的 (x,y,z)

    输出格式

    一行一个整数,表示这样的正整数个数。

    数据范围

    (1le h le 2^{63}-1, 1le x,y,zle 10^5)

    解析

    首先我们第一眼可以看出一个三个物品的完全背包,复杂度 (O(h))

    但是这个 (O(h)) 达到了 (10^{19}) 级别,显然不是正解。

    这个时候我们就要使用同余最短路来做了。

    同余最短路

    先讲一下思路的转换,也就是这个东西的推导。

    我们知道,任意一个正整数 (x) 都可以对应到一个数 (n) 的最小非负剩余为代表的完全剩余系 (S={0,1,dots,n-1}) 中的某个元素。

    而要得到这个数,我们只需要加几个模数 (n);写成带余除法的形式,就是 (x=kn+r,quad (rin Sland xmod n=r))

    我们能从中受启发,得到一个构造拼凑数字方案的方法:

    假设我们有原数 (a,b,c) ,要拼出一个数 (x)。(保证合法)

    我们可以先将所有的数放在模 (a) 意义下,然后(a,b,c) 去凑余数,凑得一个最小的正整数。(x^{prime}) 满足 (x^{prime}equiv xmod a)

    此时这个 (x^{prime})(x) 的关系有 (x^{prime}<x)(x^{prime}ge x) 两种情况。

    • (x<x^{prime})

      这意味着我们最小能凑得的与 (x) 同余的数都大于 (x),那么我们肯定不能凑出 (x)

    • (xge x^{prime})

      由于 (xequiv x^{prime}mod a) ,所以 (x=x^{prime}+ka(kin N))

    大体思路即是这样,现在我们关注具体怎么去凑这个余数。

    首先我们可以想到 DP 。

    • 状态设计:设 (f(i)) 表示给定数凑出来的模 (a) 余数为 (i) 的最小数。

    • 状态计算:由于它的来源状态较为复杂,我们考虑它被包含在了哪些集合中。

      由于凑数的时候我们可以加上 (a,b,c) 中的任意一个数,设这个数为 (t),可以得到:

      [f((i+t)mod a)=min{f((i+t)mod a) , f(i)+t} ]

      初始状态 (f(0)=0)

    以上即是朴素 DP 的过程,复杂度大概 (O(na))(n) 是给定的能拿来凑数的数的个数,在这题里面是 (3)

    我们继续观察这个 DP。

    这是一个形如 (d(y)=d(x)+z) 的转移式,我们能想到什么?

    [dis(y)=min(dis(y),dis(x)+z) ]

    这是我们在写最短路时候的式子。

    完全一样。

    将完系中的各个元素抽象为点,并将 ((i+t)mod a)(imod a) 连上权值为 (t) 的边。这就是一个最短路问题。

    我们使用了最短路将这个问题弄到了 (O(n imes aloga)dotsdots?)

    似乎反向优化了?

    但是最短路模型下这个问题的 (h) 处理范围可以达到 (10^{18}),这应该是 DP 所不能及的。

    对于这个题,我们只需要将最短路跑出来得到 (f) 数组后,将数组内的数 (f[i])(h-1) 做比较,若小于 (h-1),则答案 (Ans+=(h-1-f[i])/base+1) 即可。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    typedef pair<ll,int> PII;
    
    const int N=2e5+10;
    
    ll h;
    ll a,b,c;
    ll base;
    ll edg[N],f[N];
    
    int head[N],ver[N],nxt[N],tot=0;
    void add(int x,int y,ll z)
    {
    	ver[++tot]=y; edg[tot]=z; nxt[tot]=head[x]; head[x]=tot;
    }
    
    bool vis[N];
    priority_queue<PII,vector<PII>,greater<PII> > q;
    
    void dijkstra()
    {
    	memset(f,0x3f,sizeof f);
    	memset(vis,0,sizeof vis);
    	f[0]=0;q.push({0,0});
    	while(q.size())
    	{
    		int x=q.top().second;
    		q.pop();
    		if(vis[x]) continue;
    		vis[x]=1;
    		for(int i=head[x];i;i=nxt[i])
    		{
    			int y=ver[i];
    			if(f[y]>f[x]+edg[i])
    			{
    				f[y]=f[x]+edg[i];
    				q.push({f[y],y});
    			}
    		}
    	}
    }
    
    int main()
    {
    	scanf("%lld",&h);
    	scanf("%lld%lld%lld",&a,&b,&c);
    	base=a;
    	for(int i=0;i<base;i++)
    	{
    		add(i,(i+b)%base,b);//同余最短路的关键在于建图
    		add(i,(i+c)%base,c);
    	}
    	dijkstra();
    	ll ans=0;
    	for(int i=0;i<base;i++)
    		if(h-1>f[i]) ans+=(ll)(h-1-f[i])/base+1LL;
    	printf("%lld",ans);
    	return 0;
    }
    
    

    P2662 牛场围栏

    题目描述

    奶牛们十分聪明,于是在牛场建围栏时打算和小L斗智斗勇!小 L 有 (N) 种可以建造围栏的木料,长度分别是 (l_1,l_2, …, l_N) ,每种长度的木料无限。

    修建时,他将把所有选中的木料拼接在一起,因此围栏的长度就是他使用的木料长度之和。但是聪明的小L很快发现很多长度都是不能由这些木料长度相加得到的,于是决定在必要的时候把这些木料砍掉一部分以后再使用。

    不过由于小 L 比较节约,他给自己规定:任何一根木料最多只能削短 (M) 米。当然,每根木料削去的木料长度不需要都一样。不过由于测量工具太原始,小 L 只能准确的削去整数米的木料,因此,如果他有两种长度分别是 (7)(11) 的木料,每根最多只能砍掉 (1) 米,那么实际上就有 (4) 种可以使用的木料长度,分别是 (6,7,10, 11)

    因为小L相信自己的奶牛举世无双,于是让他们自己设计围栏。奶牛们不愿意自己和同伴在游戏时受到围栏的限制,于是想刁难一下小 L ,希望小 L 的木料无论经过怎样的加工,长度之和都不可能得到他们设计的围栏总长度。不过小 L 知道,如果围栏的长度太小,小 L 很快就能发现它是不能修建好的。因此她希望得到你的帮助,找出无法修建的最大围栏长度。

    输入格式

    输入的第一行包含两个整数(N,M),分别表示木料的种类和每根木料削去的最大值。

    以下各行每行一个整数 (l_i),表示第 (i) 根木料的原始长度。

    输出格式

    一行一个整数表示答案。若如果任何长度的围栏都可以修建或者这个最大值不存在,输出 (-1)

    输入样例

    2 1
    7 11
    

    输出样例

    15
    

    数据范围

    (40 \% :1< N< 10, 0< M< 300)

    (100 \% :1< N< 100, 0< M< 3000)

    解析

    首先我们可以将所有能用的数暴力跑出来,复杂度 (O(NM)),可以接受。

    此时我们发现第一个无解情况:当能用的数出现 (1) 的时候,可以拼凑的出任意正整数。

    将其特判掉,我们继续。

    设所有能用的数个数为 (n),那么本题的问题就是给定一个序列 ({a_n}),求 (sum_{i=1}^na_ix_i) 最大不能表示出的整数。

    我们可以考虑使用同余最短路来求。

    (base) 是我们规定的模数,同余最短路求出的距离 (f[x]) 是能拼凑出的在以 (x) 为代表元的剩余类中的最小数字。

    那么 (f[x]-base) 就是不能拼凑出的在该剩余类中的最大数字。遍历所有剩余类 ([x]) ,取最大的 (f[x]) 即可。

    这里我们会遇到另一个无解情况,可能我们根本无法拼出某一个剩余类中的数字,即 (f[x] o infty)

    仍然特判掉即可。

    #include <bits/stdc++.h>
    using namespace std;
    typedef pair<int,int> PII;
    
    const int N=4e5+10,INF=0x3f3f3f3f;
    
    int n,m;
    int arr[N],cnt=0;
    int base=INF;
    
    int head[N],ver[N<<1],nxt[N<<1],edg[N<<1],tot=0;
    void add(int x,int y,int z)
    {
    	ver[++tot]=y; edg[tot]=z; nxt[tot]=head[x]; head[x]=tot;
    }
    bool vis[N];
    int f[N];
    priority_queue<PII,vector<PII>,greater<PII> > q;
    
    void dijkstra()
    {
    	memset(f,0x3f,sizeof f);
    	memset(vis,0,sizeof vis);
    	f[0]=0; q.push({0,0});
    	while(q.size())
    	{
    		int x=q.top().second;
    		q.pop();
    		if(vis[x]) continue;
    		vis[x]=1;
    		for(int i=head[x];i;i=nxt[i])
    		{
    			int y=ver[i];
    			if(f[y]>f[x]+edg[i])
    			{
    				f[y]=f[x]+edg[i];
    				q.push({f[y],y});
    			}
    		}
    	}
    }
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    	cnt=n;
    	for(int i=1;i<=cnt;i++) scanf("%d",&arr[i]),vis[arr[i]]=1,base=min(base,arr[i]);
    	for(int i=1;i<=n;i++)//暴力跑出所有能用的数
    	{
    		for(int j=1;j<=m;j++)
    			if(arr[i]-j>0&&!vis[arr[i]-j]) arr[++cnt]=arr[i]-j,vis[arr[cnt]]=1,base=min(arr[cnt],base);
    			else if(arr[i]-j<=0) break;
    	}
    	if(vis[1]) return 0&printf("-1");//出现了 1
    	for(int i=0;i<base;i++)
    	{
    		for(int j=1;j<=cnt;j++)
    			add(i,(i+arr[j])%base,arr[j]);
    	}
    	dijkstra();
    	int ans=0;
    	for(int i=1;i<base;i++)
    	{
    		if(f[i]>=INF) return 0&printf("-1");//这个剩余类无法被拼凑出
    		ans=max(ans,f[i]-base);
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    P2371 [国家集训队]墨墨的等式

    题目描述

    墨墨突然对等式很感兴趣,他正在研究 (sum_{i=1}^n a_ix_i=b) 存在非负整数解的条件,他要求你编写一个程序,给定 (n, {a_n}, l, r),求出有多少 (bin[l,r]) 可以使等式存在非负整数解。

    输入格式

    第一行三个整数 (n,l,r)

    第二行 (n) 个整数 (a_1sim a_n)

    输出格式

    一行一个整数,表示有多少 (bin [l,r]) 可以使得等式存在非负整数解。

    输入样例

    2 5 10
    3 5
    

    输出样例

    5
    

    数据范围

    (1le nle 12 , 0<a_ile 5 imes 10^5 , 1le lle rle 10^{12})

    解析

    P3403 跳楼机 升级版。

    我们仿照那题的做法,使用同余最短路解决问题。

    对于所有的 (a_i),我们先将 (0) 筛掉,然后找到最小值,设为 (base)

    之后我们枚举所有的剩余类 ([ i ]) 和给定数组 (a_i),按照如下方式建边。

    for(int i=0;i<base;i++)
    {
        for(int j=1;j<=n;j++)
            add(i,(i+a[i])%base,a[i]);
    }
    

    这样之后,我们跑出来的最短路数组 (f[x]) 表示的就是我们能够拼出来的最小的在剩余系 ([x]) 中的数。

    现在我们考虑如何计算 ([l,r]) 中满足条件的数。

    首先我们能够轻易计算出 ([1,x]) 中满足条件的数:(sum_{i=1}^{base-1}(frac{x-f[i]}{base}+1))

    显然的是区间满足条件的数是类型与有前缀和性质的。

    所以我们只需要计算:([1,r]) 中满足条件的数 (-) ([1,l-1]) 中满足条件的数。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    typedef pair<ll,int> PII;
    
    const int N=5e6+10;
    
    int n;
    ll l,r,base=1e9;
    vector<ll> a;
    int head[N],ver[N<<1],nxt[N<<1],tot=0;
    ll edg[N<<1];
    void add(int x,int y,ll z)
    {
    	ver[++tot]=y; edg[tot]=z; nxt[tot]=head[x]; head[x]=tot;
    }
    bool vis[N];
    ll f[N];
    priority_queue<PII, vector<PII>,greater<PII> > q;
    
    void dijkstra()
    {
    	memset(f,0x42,sizeof f);
    	memset(vis,0,sizeof vis);
    	f[0]=0; q.push({0,0});
    	while(q.size())
    	{
    		int x=q.top().second;
    		q.pop();
    		if(vis[x]) continue;
    		vis[x]=1;
    		for(int i=head[x];i;i=nxt[i])
    		{
    			int y=ver[i];
    			if(f[y]>f[x]+edg[i])
    			{
    				f[y]=f[x]+edg[i];
    				q.push({f[y],y});
    			}
    		}
    	}
    }
    
    int main()
    {
    	scanf("%d%lld%lld",&n,&l,&r);
    	for(int i=1;i<=n;i++)
    	{
    		int x;
    		scanf("%d",&x);
    		if(x) a.push_back((ll)x),base=min(base,(ll)x);
    	}
    	for(int i=0;i<base;i++)
    	{
    		for(int j=0;j<a.size();j++)
    			add(i,(i+a[j])%base,a[j]);
    	}
    	dijkstra();
    	ll ansl=0,ansr=0;
    	for(int i=0;i<base;i++)
    	{
    		if(r>=f[i]) ansr+=(r-f[i])/base+1LL;
    		if(l-1>=f[i]) ansl+=(l-1-f[i])/base+1LL;]);
    	}
    	printf("%lld",ansr-ansl);
    	return 0;
    }
    

    总结

    其实同余最短路的关键是建边,这是一种通过建图来用图论算法解决非图论问题的经典例子。

    同时同余最短路也只是一种最短路的运用方式,并不是一个单独的算法。我们在题目中可以结合特定的性质来确定做法。总之,算法是死的,人是活的。

  • 相关阅读:
    android 线程的优先级设置方法
    android插件开发
    针对AsyncTask的分析
    针对平板的扩展
    adb shell dumpsys activity
    gzip相关【转帖】
    android中也需要SQL语句
    monkey脚本
    eclipse 引入外部包不能运行 NoClassDefFoundError 的问题
    翻译 : fragment
  • 原文地址:https://www.cnblogs.com/IzayoiMiku/p/14734260.html
Copyright © 2011-2022 走看看