OpenJudge NOI 1.13 19:啤酒厂选址

【题目链接】

OpenJudge NOI 1.13 19:啤酒厂选址

【题目考点】

1. 枚举

【解题思路】

一个有n个数字的环,顺时针标号从0到n-1,顺时针取下一个数字的方法为i = (i+1)%n,逆时针取下一个数字的方法为i = (i-1+n)%n

解法1:枚举

枚举,i从1循环到n,在第i个居民点建厂,求在这里建厂时从这里到所有其它居民点运送啤酒的费用加和。
从i的顺时针下一个居民点开始,环形遍历每个居民点j,记录从第i居民点顺时针到第j居民点的路程为s,环岛总路程为s_tot,那么s_tot-s就是逆时针走到第j居民点的路程。比较s与s_tot-s,二者较小值即为从i到j的最短路程距离,距离乘以这里的啤酒需求量,即为费用。
到每个居民点的费用加和,即为在第i居民点建酒厂的总费用。
求出在每个居民点建酒厂产生的费用,求最小值。
该方法复杂度 O ( n 2 ) O(n^2) O(n2)

解法2:双指针

以下叙述中,i顺时针下一个数字记为i+1,逆时针下一个数字记为i-1。
d[i]表示第i居民点到第i+1居民点的距离。
假设在居民点i建酒厂,记第j居民点是最后一个从i出发顺时针走更近的居民点,即从i出发到所有i+1~j的居民点,都是顺时针走更近。从i出发到所有j+1~i-1的居民点,都是逆时针走更近。
这里的双指针指的就是酒厂地址i与最后一个从i出发顺时针走更近的居民点j。每次让i取顺时针的下一个位置,根据j的定义调整j的位置。
同时调整顺时针能走到的最远距离sr,逆时针能走的最远距离sl,顺时针到达的居民点需要的啤酒总数量br,逆时针到的居民点需要的啤酒总数量bl,以及总费用pr。
首先求出在第0位置建酒厂时,j, sr, sl, br, bl, pr各项的数值。

k从1顺时针遍历,sr不断增大,直到如果顺时针到k的距离sr+d[k-1]大于逆时针到k的距离s_tot-sr-d[k-1],那么就应该逆时针到k,j应该取k-1。同时更新br,pr。
再从n-1开始逆时针遍历到j+1,sl不断增大。同时更新bl,pr。

建酒厂的位置从1遍历到n-1,当建厂位置变为顺时针的下一个位置后,

  • 如果酒厂在i-1位置时对所有的居民点都是逆时针走到的,那么满足 j = i − 1 j = i-1 j=i1。酒厂移动到第i位置后,逆时针到达的居民点减少了第i居民点,增加了第i-1居民点。
  • 除了以上情况,酒厂移动到第i位置后,顺时针到达的居民点减少了第i居民点,逆时针到达的居民点增加了第i-1居民点。

根据不同情况调整sr, sl, br, bl, pr。

此时i已经增加了1,看i到哪些定居点的行进方向发生了变化,也就是看j的变化。
j变为顺时针的下一个,看当前第i居民点到第j居民点是顺时针走更近还是逆时针走更近。

  • 如果是顺时针走更近,那么总费用减少了从i-1逆时针走送啤酒到j的费用,增加了从i顺时针走送啤酒到j的费用。而后让j增加1,再看到下一个居民点是否需要变换行进方向。
  • 如果到第j居民点是逆时针走更近,那么让j回到上一个位置,始终让j表示最后一个从i出发顺时针走更近的居民点,结束循环。
  • 以上过程进行中,需要不断调整sr, sl, br, bl, pr的值。

求出在第i位置建酒厂的总费用pr后,与已知的最小总费用左比较,求出费用的最小值。
该方法复杂度 O ( n ) O(n) O(n)

【题解代码】

解法1:枚举 复杂度: O ( n 2 ) O(n^2) O(n2)
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define N 10005
int main()
{
    int s_tot = 0, n, a[N], d[N], mn = INF, mi = 0;//tot:环岛总长,a[i]:第i居民点啤酒需求量,d[i]为i到i+1位置的举例,p[i]在第i位置建厂每天运啤酒的费用, c单位费用
    scanf("%d", &n);
    for(int i = 0; i < n; ++i)
    {
        scanf("%d %d", &a[i], &d[i]);
        s_tot += d[i];
    }
    for(int i = 0; i < n; ++i)//遍历居住点
    {
        int f = 0, s = 0, sd;//f:总费用 j:当前看的居住点 s:累积距离 sd:从j到i的最短距离
        for(int j = (i+1)%n; j != i; j = (j+1) % n)//从i的下一个居住点开始,环形遍历每个居住点 
        {
            s += d[(j-1+n)%n];
            sd = s_tot - s < s ? s_tot - s : s;
            f += sd * a[j];
        }
        if(f < mn)
        {
            mn = f;
            mi = i;
        }
    }
    printf("%d,%d", mi, mn);
    return 0;
}
解法2:双指针 复杂度: O ( n ) O(n) O(n)
#include<bits/stdc++.h>
using namespace std;
#define N 10005
int main()
{
    int s_tot = 0, n, b[N], d[N];//b[i]:第i居民点的啤酒需求量 d[i]:第i居民点到第i+1居民点的距离 
    scanf("%d", &n);
    for(int k = 0; k < n; ++k)
    {
        scanf("%d %d", &b[k], &d[k]);
        s_tot += d[k];//s_tot:环总长
    }
    int i = 0, j = n-1;//i:在第i位置建厂 j:从酒厂顺时针走到达更近的最后一个居民点,从酒厂到(j+1)%n居民点就是逆时针走更近。如果下面循环结束也没有设j,那么所有顶点都是通过顺时针走到的,j应该为n-1 
    int bl = 0, br = 0;//br:顺时针走到的居民点的啤酒需求量 bl:逆时针走到的居民点的啤酒需求量 
    int sl = 0,  sr = 0;//sr:从i开始顺时针走过的距离  sl:从i开始逆时针走过的距离 
    int pr = 0;//总费用
    for(int k = 1; k < n; k++)
    {
        if(sr + d[k-1] <= s_tot - sr - d[k-1])//如果从i顺时针走到k的距离小于等于逆时针走到k的距离 
        {
            sr += d[k-1];
            br += b[k];//顺时针走到的居民点的啤酒需求量增加 
            pr += sr * b[k];//费用增加 
        }
        else
        {
            j = k-1;//取前一个位置 为顺时针能走到的最后一个位置 
            break;
        }
    }
    for(int k = n-1; k != j; k--)//从i的前一个开始逆时针遍历直到j
    {
        sl += d[k];//逆时针走的距离 
        pr += sl * b[k];//此时sl为逆时针走到k的距离 
        bl += b[k];
    }
    int mnp = pr, mni = 0;//此时pr为在第0居民点建厂的总费用。mnp:费用最小值 mni:使得费用最小的啤酒厂位置 
    for(i = 1; i <= n-1; ++i)//酒厂从i-1移动到i
    { 
        if(j == (i-1)%n)//在i-1时,如果所有位置都要逆时针走到,j等于i逆时针的下一个。 
        {//酒厂移动到第i位置后,逆时针到达的居民点减少了第i居民点,增加了第i-1居民点。
            j = i;
            bl = bl - b[i] + b[i-1];
            pr += bl * d[i-1] - b[i] * sl;//bl的啤酒需求运送距离增加d[i-1]。向第i居民点原来需要逆时针走sl送啤酒,现在不用送了。 
            sl += d[i-1] - d[i];
        }
        else
        {//酒厂移动到第i位置后,顺时针到达的居民点减少了第i居民点,逆时针到达的居民点增加了第i-1居民点。
            sr -= d[i-1];
            sl += d[i-1];
            bl += b[i-1];
            pr += (bl - br) * d[i-1];//顺时针移动一个位置后,对于br的啤酒需求,距离变短,对于bl的啤酒需求,距离变长。 
            br -= b[i];
        }
        while(sr + d[j] < s_tot - sr - d[j])//如果到第j居民点顺时针走距离更短 
        {
            sr += d[j];
            j = (j + 1) % n;
            bl -= b[j];//到第j居民点从逆时针走到变为顺时针走到
            br += b[j];
            pr += (sr - sl) * b[j];//此时sl为i逆时针到j的距离,sr为i顺时针到j的距离 
            sl -= d[j];
        }
        if(pr < mnp)
        {
            mnp = pr;
            mni = i;
        }
    }
    printf("%d,%d", mni, mnp); 
    return 0;
}

【测试数据】

亲测解法2确实降低了复杂度,这里给一个生成数据的程序。由于数据太大会让结果超出int范围,这里人为缩小了数据的范围(每个居民点啤酒需求小于等于200,两个居民点间的距离小于等于10)。

#include <bits/stdc++.h>
using namespace std;
int main()
{
    srand(time(NULL));
    freopen("test.txt", "w", stdout);
    int n = 10000;
    cout << n << endl;
    for(int i = 1; i <= n; ++i)
    {
        int a = rand()%200+1, b = rand()%10+1;
        cout << a << ' ' << b << endl; 
    }
    fclose(stdout);
    return 0;
}

生成了一组n为10000的数据进行测试,解法1的代码需要运行1s以上,而解法2的代码总能在1s内完成。
本题正解应该是复杂度为 O ( n ) O(n) O(n)的解法2。由于OJ数据较水,因此复杂度为 O ( n 2 ) O(n^2) O(n2)的解法1也能过。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值