CODEVS 1183 泥泞的道路 二分+01分数规划(双倍经验)

水题你们是不是看够了?
很好,拔剑吧!,接下来是个小BOSS,泥泞的道路。

题目描述 Description
CS有n个小区,并且任意小区之间都有两条单向道路(a到b,b到a)相连。因为最近下了很多暴雨,很多道路都被淹了,不同的道路泥泞程度不同。小A经过对近期天气和地形的科学分析,绘出了每条道路能顺利通过的时间以及这条路的长度。
现在小A在小区1,他希望能够很顺利地到达目的地小区n,请帮助小明找出一条从小区1出发到达小区n的所有路线中“(总路程/总时间)“ 最大的路线。请你告诉他这个值。

输入描述 Input Description
第一行包含一个整数n,为小区数。
接下来n*n的矩阵P,其中第i行第j个数表示从小区i到小区j的道路长度为Pi,j。第i行第i个数的元素为0,其余保证为正整数。
接下来n*n的矩阵T,第i行第j个数表示从小区i到小区j需要的时间Ti,j。第i行第i个数的元素为0,其余保证为正整数。

输出描述 Output Description
写入一个实数S,为小区1到达n的最大答案,S精确到小数点后3位。

样例输入 Sample Input
3
0 8 7
9 0 10
5 7 0

0 7 6
6 0 6
6 2 0

样例输出 Sample Output
2.125

数据范围及提示 Data Size & Hint
【数据说明】
30%的数据,n<=20
100%的数据,n<=100,p,t<=10000

这题咋一看非常的迷….根本无从下手啊,是从路程开始二分,还是从时间开始二分呢?
答案是:都不需要。

接下来就要看你们的数学能力了。
请帮助小明找出一条从小区1出发到达小区n的所有路线中“(总路程/总时间)“ 最大的路线。
设路程数组为dis(区别于下面的dist!),时间为(tim),
设一个数组x,x[i]=1表示第i条边在所选的路线中,x[i]=0表示第i条边不在路线中,(所以,dis[i],tim[i],为定值,x[i]不是定值,x[i]会随着你跑spfa而变,下面会提到)

很好,假设 我们现在已经知道答案了,就是
R=( i=1ndis[i]x[i] )/( i=1ntim[i]x[i] )
i=1ndis[i]x[i] 就是我们的最优解情况下总路程
i=1ntim[i]x[i] 就是我们的最优解情况下总时间
处理一下就是:
R*( i=1ntim[i]x[i] )=( i=1ndis[i]x[i] )

那么( i=1ndis[i]x[i] )-R*( i=1ntim[i]x[i] )=0
↑在下面会提到

在这里R是我们的”最终解“!
在这里R是我们的”最终解“!
在这里R是我们的”最终解“!
重要的话说三遍。

定义一个函数f(L)=( i=1ndis[i]x[i] )-L*( i=1ntim[i]x[i] )
我们需要让f(L)不断接近R,直到f(L)==R

化简一下就是f(L)=( i=1nx[i]dis[i]Ltim[i])
设d[i]=dis[i]-L*tim[i];
则f(L)=( i=1nx[i]d[i] )

此时的d数组就相当于我们每条边的权值。
我们可以这样想:当前的f(L)相当于当前解,因为它是跑spfa过来的,所以选取了其中的一些边(最优边),因为是从节点1跑到节点n,那么dist[n]==( i=1nx[i]d[i] )。)==f(L)(区别于上面的dis!,这里的dist是跑spfa时记录最短路的数组(本题中为最长路)
巧妙的把这个题转化成了最长路问题

请帮助小明找出一条从小区1出发到达小区n的所有路线中“(总路程/总时间)“ 最大的路线。
↑所以这题是跑最长路,该是能看的懂吧(跑最长路的过程中已经把不存在于代码中的x[i]的值求出来了),
当f(L)==0时,(看上面)L就是我们要求的R,就是最优解。
当f(L)>0时,我们推一下(这个地方有点绕,请认真思考,反正我一开始是一脸懵逼),
f(L)=( i=1ndis[i]x[i] )-L*( i=1ntim[i]x[i] )且f(L)>0,
所以( i=1ndis[i]x[i] )>L*( i=1ntim[i]x[i] )
所以( i=1ndis[i]x[i] )/( i=1ntim[i]x[i] )>L,
说明,对于一个确定的参数L来说,如果一个方案使得f(L)>0那么这组方案可以得到一个比现在的L更优的一个L,
L如果增大的话,得到的解更优,L需要增大对吧!

那么什么时候f(L)>0呢?
有两种情况:
1、dist[n]>0
上面已经说了,dist[n]就相当于L,所以dist[n]>0时,L就大于0咯。
2、存在正环
因为本题是跑最长路嘛,如果你找到一个正环,在里面绕个几亿亿亿遍,当前的权值肯定是非常大的,还怕跑到节点n时dist[n]小于零?

对于这个题来说…..二分倒不是那么难打了,难就难在上面的推公式上。

我知道你们看不懂我在说啥QwQ…..戳这里看看吧

db div()
{
    db l = 0,r = 100000000;
    db ans=0;
    int k = 0;
    while(k <= 100)
    {
        double mid = ((l + r) / (double)(2));
        if(!spfa(mid))  
            r = mid;
        else    
            l = mid,ans=max(ans,mid);
        k ++;
      //  printf("l:%.1lf r:%.1lf\n",l,r);
    }
    return ans;
}

浮点数二分部分….和平常的整数二分区别很大,关于里面的k<=100,我只想说:
2^100=
2^100=
本题这么小的数据,还怕分不完?

如果精度大的话,把100往上提就是了,有个科学家说过,如果能把全宇宙的粒子按质量从小到大排序,你如果想找宇宙中的任何一个粒子,使用二分查找,只七百多次就足够了。

还怕耗不过你的数据?

#include<cstdio>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
typedef double db;
const int maxn=20000;
const db inf=0x7ffffff;
struct Edge
{
    int f;
    int to;
    db d;
    db tim;
    int next;
}edge[maxn];
int head[maxn],time[maxn];
db dist[maxn];
bool vis[maxn];
int n;
db dis[150][150],tim[150][150];
int tot;
void add(int f,int t,db d,db tt)
{
    edge[++tot].to=t;
    edge[tot].d=d;
    edge[tot].tim=tt;
    edge[tot].next=head[f];
    head[f]=tot;
}
deque<int>q;
void clr()
{
    tot=0;
    while(!q.empty())
        q.pop_front();
    for(int i=1;i<=n;i++)
        dist[i]=(-1)*inf;
    memset(vis,0,sizeof(vis));
    memset(time,0,sizeof(time));
//  printf("%lf\n",dist[1]);
}
bool spfa(db mid)
{
    clr();
    q.push_front(1);
    dist[1]=0.0;
    while(!q.empty())
    {
        int x=q.front();
        q.pop_front();
        vis[x]=0;
        for(int i=head[x];i;i=edge[i].next)
        {
            Edge e=edge[i];
            if(dist[e.to]<dist[x]+(e.d-mid*e.tim))
            {
                dist[e.to]=dist[x]+(e.d-mid*e.tim);
                if(!vis[e.to])
                {
                    vis[e.to]=true;
                    time[e.to]++;
                    if(time[e.to]>n)
                    {
                        return true;
                    }
                    if(!q.empty()&&dist[e.to]>dist[q.front()])
                        q.push_front(e.to);
                    else
                        q.push_back(e.to);
                }
            }
        }
    }
    if(dist[n]>0)
        return true;
    return false;
}
db div()
{
    db l = 0,r = 100000000;
    db ans=0;
    int k = 0;
    while(k <= 100)
    {
        double mid = ((l + r) / (double)(2));
        if(!spfa(mid))  
            r = mid;
        else    
            l = mid,ans=max(ans,mid);
        k ++;
      //  printf("l:%.1lf r:%.1lf\n",l,r);
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%lf",&dis[i][j]);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%lf",&tim[i][j]);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(i!=j)
            {
                add(i,j,dis[i][j],tim[i][j]);
            }
        }
    }
    printf("%.3f",div());       
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值