poj Desert King ---- 最小比率生成树(0/1 分数规划)

题意简化如下:

给定 n n n个村庄的坐标 ( n < = 1000 ) \left(n <= 1000 \right) (n<=1000)与高度,两个村庄之间的距离是两点的欧式距离, 两个村庄修一条路需要花费两个村庄高度差的价钱,现在你要修若干条路,让任意一个村庄总能通过一条路到另一个村庄, 求出道路总成本与总长度比值的最小值。

算法实现:

很明显是0/1分数规划, 题意是选出一棵树使得道路总成本与总长度比值最小,设 x i ∈ ( 0 , 1 ) x_i \in \left( 0, 1 \right) xi(0,1) 表示第i条路选或者不选, 选就是 x i = 1 x_i = 1 xi=1,不选就是 x i = 0 x_i = 0 xi=0, m m m为总边数, p i p_i pi表示第 i i i条边的成本, l i l_i li表示第 i i i条边的长度.若答案是 a n s ans ans, 那么有 ∑ i = 0 m x i p i ∑ i = 0 m x i l i > = a n s \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} >= ans i=0mxilii=0mxipi>=ans,即从 m m m条边中选出任意 n − 1 n - 1 n1条边,一定会有 ∑ i = 0 m x i p i ∑ i = 0 m x i l i > = a n s \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} >= ans i=0mxilii=0mxipi>=ans.设我们当前要判断的答案值为 m i d mid mid,那么若存在一组解 x i x_i xi使得 ∑ i = 0 m x i p i ∑ i = 0 m x i l i < m i d \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} i=0mxilii=0mxipi<mid,说明 m i d > a n s mid > ans mid>ans,若对任意一组解 x i x_i xi都有 ∑ i = 0 m x i p i ∑ i = 0 m x i l i > = m i d \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} >= mid i=0mxilii=0mxipi>=mid,说明 m i d < = a n s mid <= ans mid<=ans,明显具有单调性,所以我们不妨二分答案然后求解.
由于分式不好求解,我们将上式移项化简得: ∑ i = 0 m x i p i − m i d ∗ x i l i > = 0 \sum_{i = 0}^mx_ip_i - mid *x_il_i >=0 i=0mxipimidxili>=0,其中 x i x_i xi n − 1 n - 1 n1项为 1 1 1,其余项为 0 0 0,若上式最小值小于 0 0 0,说明对上式任意的组合,其值都小于0,反之亦成立.我们建一张新图,设新图边权为 x i p i − m i d ∗ l i x_ip_i - mid *l_i xipimidli,上式最小值可由 P r i m Prim Prim算法或者 K r u s k a l Kruskal Kruskal算法求出

复杂度分析

二分答案的上界为 N N N,边数为 n 2 n^2 n2,点数为 n n n,若使用 K r u s k a l Kruskal Kruskal算法,其复杂度为 O ( 2 n 2 l o g ( n ) l o g ( N ) ) O(2n^2log(n)log(N)) O(2n2log(n)log(N)),最坏情况下约为 5 e 8 5e8 5e8,在没有卡常的情况下很难通过.

若使用堆优化的 P r i m Prim Prim算法,其复杂度为 O ( n 2 l o g ( n ) l o g ( N ) ) O(n^2log(n)log(N)) O(n2log(n)log(N)),依然很难通过.

若使用朴素的 P r i m Prim Prim算法,其复杂度为 O ( n 2 l o g ( N ) ) O(n^2log(N)) O(n2log(N)),最坏情况下约为 2 e 7 2e7 2e7,比上述算法快了十倍左右.

代码实现

经过上述分析发现在本题中出现了越优化算法越慢的情况,反而朴素的 P r i m Prim Prim算法更优.

代码如下

#include
#include
#include
using namespace std;
int n, m;
const int N = 1e6 + 5;
struct Edge{
    int a, b;
    double w;
    bool operator < (const Edge& t) const{
    return w > t.w;
}
}ed[N];
int price[1005][1005];
double Dist[1005][1005], d[1005];
bool st[1005];
inline double dist(int x, int y){
    if(Dist[x][y]) return Dist[x][y];
    return Dist[x][y] = sqrt((double)(ed[x].a - ed[y].a) * (ed[x].a - ed[y].a) + (ed[x].b - ed[y].b) * (ed[x].b - ed[y].b));
}
inline double get(int i, int j, double mid){
    return price[i][j] - mid * dist(i, j);
}
bool check(double mid){
    double res = 0;
    memset(st, 0, sizeof st);
    for(int i = 0;i < n; i++) d[i] = 1e18;
   // cout << d[1] << endl;
    d[0] = 0;
    for(int i = 0;i < n; i++){
        int t = -1;
        for(int j = 0;j < n; j++) if(!st[j] && (t == -1 || d[t] > d[j])) t = j;
        res += d[t];
        st[t] = 1;
        for(int i = t + 1;i < n; i++) {
            double D = get(t, i, mid);
            if(d[i] > D) {
            d[i] = D;
        }
        }
        for(int i = 0;i < t; i++) {
            double D = get(i, t, mid);
            if(d[i] > D) {
            d[i] = D;
        }
        }
    }
    return res <= 0;
}
void read(){
    for(int i = 0;i < n; i++){
        int a, b;
        double c;
        scanf("%d%d%lf", &a, &b, &c);
        ed[i].a = a, ed[i].b = b, ed[i].w = c;
    }
    for(int i = 0;i < n; i++)
      for(int j = i + 1;j < n; j++) price[i][j] = abs(ed[i].w - ed[j].w);
    double l = 0, r = 1e7;
    while(r - l > 1e-4){
        double mid = (l + r) / 2;
        if(check(mid)) r = mid;
        else l = mid;
    }
    printf("%.3lf\n", r);
}
void solve(){
    memset(Dist, 0, sizeof Dist);
    read();
}
int main(){
    while(scanf("%d", &n), n) solve();
    return 0;
}

但是二分在不卡常时会超时.在这里插入图片描述于是想到另一种0/1分数规划的算法, D i n k e l b a c h Dinkelbach Dinkelbach算法,该算法的复杂度在此题中最坏为 O ( 3 n 2 l o g ( n ) ) O(3n^2log(n)) O(3n2log(n)),但是实际上绝大多数情况到不了这个复杂度,总体比二分会快不少.二分最坏其实也就是 1 0 7 10^7 107级别,卡常之后还要跑1500ms感觉有点玄学,也许是我时间复杂度算错了?

#include
#include
#include
#include
using namespace std;
int n, m;
const int N = 1e6 + 5;
struct Edge{
    int a, b;
    double w;
}ed[N];
int price[1005][1005];
double Dist[1005][1005], d[1005], len, cost;
int pre[1005];
bool st[1005];
double check(double mid){
    len = 0, cost = 0;
    memset(st, 0, sizeof st);
   // for(int i = 0;i < n; i++) d[i] = 1e18;
   // cout << d[1] << endl;
    d[0] = 0;
    for(int i = 1;i < n; i++) d[i] = price[0][i] - mid * Dist[0][i], pre[i] = 0;
    for(int i = 0;i < n; i++){
        int t = -1;
        for(int j = 1;j < n; j++) if(!st[j] && (t == -1 || d[t] > d[j])) t = j;
        if(t == -1) break;
        if(t){
        len += Dist[pre[t]][t];
        cost += price[pre[t]][t];
        }
        st[t] = 1;
        for(int i = t + 1;i < n; i++) {
            double D = price[t][i] - mid * Dist[t][i];
            if(d[i] > D) {
            d[i] = D;
            pre[i] = t;
        }
        }
        for(int i = 1;i < t; i++) {
            double D = price[i][t] - mid * Dist[i][t];
            if(d[i] > D) {
            d[i] = D;
            pre[i] = t;
        }
        }
    }
    return cost / len;
}
void read(){
    for(int i = 0;i < n; i++){
        int a, b;
        double c;
        scanf("%d%d%lf", &a, &b, &c);
        ed[i].a = a, ed[i].b = b, ed[i].w = c;
    }
    for(int i = 0;i < n; i++)
      for(int j = i + 1;j < n; j++) price[i][j] = price[j][i] = abs(ed[i].w - ed[j].w),Dist[i][j] = Dist[j][i] = sqrt((double)(ed[i].a - ed[j].a) * (ed[i].a - ed[j].a) + (ed[i].b - ed[j].b) * (ed[i].b - ed[j].b));
    double r = 0, rt = 0;
    while(1){
        rt = check(r);
        if(fabs(r - rt) < 1e-5) break;
        r = rt;
    }
    printf("%.3lf\n", r);
}
void solve(){
    read();
}
int main(){
    while(scanf("%d", &n), n) solve();
    return 0;
}

两种方式时间的对比
在这里插入图片描述

你可能感兴趣的:(poj Desert King ---- 最小比率生成树(0/1 分数规划))