01分数规划 总结报告

01分数规划

参考:
http://www.cnblogs.com/perseawe/archive/2012/05/03/01fsgh.html
胡伯涛:《最小割模型在信息学竞赛中的应用》(强力推荐)

定义

分数规划是一类问题。而01分数规划是分数规划的一个特例。
分数规划的一般形式: λ=f(x)=a(x)b(x),(xS) ,求 λ 最大或者最小。其中,解向量 x 在解空间S内, a(x)b(x) 都是连续的实值函数。
01分数规划,就是解空间是 {0,1} 的分数规划问题,其实就是对一个条件,取还是不取进行选择。

转化

这里以最小化问题进行解释。
λ 为最优解。
λ=f(x)=a(x)b(x),(xS)
=>λb(x)=a(x)
=>0=a(x)λb(x)
由上面的形式可以构造一个新函数 g(λ) :
g(λ)=minxS{a(x)λb(x)}

对之所以取小做出解释:
λ=f(x)=a(x)b(x)a(x)b(x)
=>a(x)λb(x)0
x=x 时取最优解,且为0,所以,应该取小。

性质

  1. 单调性: g(λ) 是一个严格递减函数。
  2. Dinkelbach定理: λ 为原规划的最优解,则 g(λ)=0 当且仅当 λ=λ
  3. 推论:
    {g(λ)=0λ=λ, g(λ)<0λ>λ, g(λ)>0λ<λ

可以验证,无论是最小化问题还是最大化问题, g(λ) 的单调性都为单调递减。

解题步骤

找出分数规划不等式关系,写出 g(λ) 函数,尤其注意确定取大还是取小,二分枚举 λ ,验证答案,验证答案的策略一般是求 g(λ) 与0的接近程度,利用单调性逐步接近答案;验证答案的方法,根据题目类型,应用不同的算法,下面题解提供了几类。
还有一种方法,Dinkelbach。

求01分数规划的另一个方法就是Dinkelbach算法,他就是基于这样的一个思想,他并不会去二分答案,而是先随便给定一个答案,然后根据更优的解不断移动答案,逼近最优解。由于他对每次判定使用的更加充分,所以它比二分会快上很多。但是,他的弊端就是需要保存这个解,而我们知道,有时候验证一个解和求得一个解的复杂度是不同的。二分和Dinkelbach算法写法都非常简单,各有长处,大家要根据题目谨慎使用。

个人总结

  1. 题目中求最大,则 g(λ) 取大,否则取小。
  2. 转换后,解题用到的算法,和原题算法一致。
  3. 01分数规划只是一种解题的思路,相应算法和01分数规划的方法结合,解决问题。

例题

一、

POJ 2976 Dropping tests
解析:简单的01分数规划, g(λ)=max{(aλb)x},x{0,1}
有a、b数组和枚举的 λ ,得到新的w数组, w[i]=a[i]λb[i] ,因为要取最大,所以,从大到小排序,取前n-m个,看与0的大小关系,直至上下限小于误差范围。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm> 
#define INF 0x3f3f3f3f
#define EPS 1e-6
#define N 1005
#define M 1005
using namespace std;

int n, m;
double u[M], v[M];
double w[N];

bool cmp(double a, double b) {
    return a > b;
}
bool check(double mid) {
    double ans = 0;
    for(int i = 0; i < n; i++)
        w[i] = u[i]-mid*v[i];
    sort(w, w+n, cmp);
    for(int i = 0; i < n-m; i++)
        ans += w[i];
    return ans <= EPS;
}

int main() {
    while(scanf("%d%d", &n, &m)) {
        if(!n && !m) break;     
        double l = 0, r = 0;

        for(int i = 0; i < n; i++) {
            scanf("%lf", &u[i]);
        }
        for(int i = 0; i < n; i++) {
            scanf("%lf", &v[i]);
            r = max(r, u[i]*1./v[i]);
        }

        while( fabs(r-l) > EPS) {
            double mid = (l+r)/2.0;
            if(check(mid))
                r = mid;
            else
                l = mid;
        }

        printf("%.0f\n", r*100);
    }

    return 0;
}

二、

ZOJ 2676 Network Wars
解析:01规划与最小割结合。 g(λ)=min{(wλ)x},x{0,1} ,每次枚举 λ ,重新建图,边权为 wλ ,如果 wλ<0 ,因为答案去最小且最小割算法只能在正权图上跑,所以直接算入答案,其他边,跑最小割算法,两部分答案相加,与0进行比较,逐步接近答案。
这一题卡时间很厉害,调了好久自己的最大流模板。
标记的方法也很巧妙。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#define INF 0x3f3f3f3f
#define EPS 1e-8
#define N 110
#define M 2000
using namespace std;

struct edge {
    int to, next;
    double c, f;
}graph[M];
int totlen;
int head[N];

int n, m;
int u[M], v[M];
double w[M];

void init() {
    totlen = 0;
    memset(head, -1, sizeof(int)*n);
}

void addEdge(int u, int v, double w) {
    graph[totlen] = {v, head[u], w, 0};
    head[u] = totlen++;
    graph[totlen] = {u, head[v], 0, 0};
    head[v] = totlen++;
}

int que[N<<1];
int cur[N];
bool visit[N];
int level[N];
bool dinic_bfs(int s, int t) {
    int front = 0, tail = 0;
    memset(level, 0, sizeof level);
    level[s] = 1;
    que[tail++] = s;
    while(front != tail) {
        int u = que[front++];
        if(front == (N<<1)) front = 0;
        for(int i = head[u]; i!=-1; i = graph[i].next) {
            int v = graph[i].to;
            if(!level[v] && graph[i].c-graph[i].f > EPS) {
                level[v] = level[u]+1;
                if(v == t) return true;
                que[tail++] = v;
                if(tail == (N<<1)) tail = 0;
            }
        }
    }
    return false;
}

double dinic_dfs(int u, int t, double cpflow) {
    if(u == t || cpflow < EPS) return cpflow;
    double addflow = 0;
    for(int i = head[u]; i != -1; i = graph[i].next) {
        int v = graph[i].to;
        if(level[v] == level[u]+1 && graph[i].c-graph[i].f > EPS) {
            addflow = dinic_dfs(v, t, min(graph[i].c-graph[i].f, cpflow-addflow));
            if(addflow > EPS) {
                graph[i].f += addflow;
                graph[i^1].f -= addflow;
                return addflow;
            }
        }
    }
    return addflow;
}

double dinic(int s, int t) {
    double maxflow = 0;
    double tmpflow = 0;
    while(dinic_bfs(s, t)) {
        while((tmpflow = dinic_dfs(s, t, INF)) > EPS)
            maxflow += tmpflow;
    }
    return maxflow;
}

bool check(double mid) {
    init();
    double ans = 0;
    for(int i = 0; i < m; i++) {
        if( w[i]-mid <= EPS)
            ans += w[i]-mid;
        else {
            addEdge(u[i], v[i], w[i]-mid);
            addEdge(v[i], u[i], w[i]-mid);
        }
    }
    ans += dinic(1, n);
    return ans <= EPS;
}

void dfs_mark(int u) {
    visit[u] = true;
    for(int i = head[u]; i != -1; i = graph[i].next) {
        int v = graph[i].to;
        if(!visit[v] && graph[i].c-graph[i].f > EPS)
            dfs_mark(v);
    }
}

int main() {
    int iCase = 0;
    while(scanf("%d%d", &n, &m) != EOF) {
        if(iCase++) printf("\n");
        double l = INF, r = 0;

        init();
        for(int i = 0; i < m; i++) {
            scanf("%d%d%lf", &u[i], &v[i], &w[i]);
            l = min(l, w[i]);
            r = max(r, w[i]);
        }

        while( r-l > EPS) {
            double mid = (l+r)/2.0;
            if(check(mid))
                r = mid;
            else
                l = mid;
        }

        memset(visit, 0, sizeof(int)*n);
        dfs_mark(1);
        int ans[M];
        int cnt = 0;
        for(int i = 0; i < m; i++) {
            if((visit[u[i]] ^ visit[v[i]]) || w[i] <= r) 
                ans[cnt++] = i+1;
        }
        printf("%d\n", cnt);
        for(int i = 0; i < cnt-1; i++) {
            printf("%d ", ans[i]);
        }
        printf("%d\n", ans[cnt-1]);
    }

    return 0;
}

三、

POJ 2728 Desert King
解析:最优比例树,01分数规划与最小生成树结合。
g(λ)=min{(dzλdist)x},x{0,1} ,每次枚举 λ ,重新建图,边权为 dzλdist ,然后找最小生成树,因为是接近完全图,所以Prim要比Kruskal快一些。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#define INF 0x3f3f3f3f
#define EPS 1e-6
using namespace std;
typedef pair<double, int> P;
const int N = 1005;
const int M = N*N;

struct edge {
    int to, next;
    double w;
}graph[M];
int totlen;
int head[N];

void addEdge(int u, int v, double w) {
    graph[totlen] = {v, head[u], w};
    head[u] = totlen++;
}

struct node {
    int x, y, z;
}coor[N];
double dist[N][N];

int n;

double cost, dis;

double lowcost[N];
int pre[N];
bool visit[N];
priority_queue<P, vector<P>, greater<P> > que;
double Prim(int s) {
    while(!que.empty()) que.pop();
    double sumcost = 0;
    memset(visit, 0, sizeof visit);
    for(int i = 0; i < n; i++) lowcost[i] = INF;

    lowcost[s] = 0;
    pre[s] = s;
    que.push(P(0, s));
    while(!que.empty()) {
        P cur = que.top();  que.pop();
        int u = cur.second;
        if(visit[u] || lowcost[u] < cur.first) continue;

        sumcost += lowcost[u];
        cost += fabs(coor[u].z-coor[pre[u]].z);
        dis += (u < pre[u] ? dist[u][pre[u]] : dist[pre[u]][u]);
        visit[u] = 1;

        for(int i = head[u]; i != -1; i = graph[i].next) {
            int v = graph[i].to;
            if(!visit[v]  && lowcost[v] > graph[i].w) {
                lowcost[v] = graph[i].w;
                pre[v] = u;
                que.push(P(lowcost[v], v));
            }
        }
    }
    return sumcost;
}

bool check(double mid) {
    totlen = 0;
    memset(head, -1, sizeof head);
    cost = dis = 0;

    double ans = 0;
    for(int i = 0; i < n; i++) {
        for(int j = i+1; j < n; j++) {
            addEdge(i, j, fabs(coor[i].z-coor[j].z)-mid*dist[i][j]);
            addEdge(j, i, fabs(coor[i].z-coor[j].z)-mid*dist[i][j]);
        }
    }
    ans += Prim(0);
    return ans <= EPS; 
}

double calDist(node a, node b) {
    double t1 = (a.x-b.x)*1.*(a.x-b.x);
    double t2 = (a.y-b.y)*1.*(a.y-b.y);
    return sqrt(t1+t2);
}

int main() {
    while(scanf("%d", &n) && n) {
        for(int i = 0; i < n; i++) {
            scanf("%d%d%d", &coor[i].x, &coor[i].y, &coor[i].z);
        }
        for(int i = 0; i < n; i++) {
            dist[i][i] = 0;
            for(int j = i+1; j < n; j++)
                dist[i][j] = calDist(coor[i], coor[j]);
        }

        double ans = 0;
        double l = 2;
        while(fabs(ans-l) > EPS) {
            ans = l;
            check(l);
            l = cost/dis;
        }

        printf("%.3f\n", ans);
    }
    return 0;
}

四、

POJ 3621 Sightseeing Cows
解析:最优比率环,01分数规划与判负环结合。
g(λ)=max{(fλt)x},x{0,1} ,因为最后一定要构成一个环,所以枚举 λ 后,建图时,边权可以为 f[v[i]]λt ,走一圈后,刚好所有点的f都计算了,所有边的t都计算了。
这一题就不需要与0进行比较了,只要有正环,此时 g(λ)>0 没有正环,则为 g(λ)<0 ,逐步接近就好。

分析:比上面更加的恶心了。先不说环的问题,就是花费和收益不在一处也令人蛋疼。这时候需要用到几个转化和结论。
首先的一个结论就是,不会存在环套环的问题,即最优的方案一定是一个单独的环,而不是大环套着小环的形式。这个的证明其实非常的简单,大家可以自己想一下(提示,将大环上的收益和记为x1,花费为y1,小环上的为x2,y2。重叠部分的花费为S。表示出来分类讨论即可)。有了这个结论,我们就可以将花费和收益都转移到边上来了,因为答案最终一定是一个环,所以我们将每一条边的收益规定为其终点的收益,这样一个环上所有的花费和收益都能够被正确的统计。
解决了蛋疼的问题之后,就是01分数规划的部分了,我们只需要计算出D数组后找找有没有正权环即可,不过这样不太好,不是我们熟悉的问题,将D数组全部取反之后,问题转换为查找有没有负权环,用spfa或是bellman_ford都可以。这道题目就是典型的不适合用Dinkelbach,记录一个负权环还是比较麻烦的,所以二分搞定。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#define INF 0x3f3f3f3f
#define EPS 1e-6
using namespace std;
const int N = 1005;
const int M = 5005;

struct edge {
    int to, next;
    double w;
}graph[M];
int totlen;
int head[N];

int n, m;
int a[M], w[M];
int u[M], v[M];

void init() {
    totlen = 0;
    memset(head, -1, sizeof head); 
}

void addEdge(int u, int v, double w) {
    graph[totlen] = {v, head[u], w};
    head[u] = totlen++;
}

bool visit[N];
double dist[N]; // 初始化为0
bool spfa_dfs(int u) {
    visit[u] = 1;
    for(int i = head[u]; i != -1; i = graph[i].next) {
        int v = graph[i].to;
        if(dist[v] > dist[u]+graph[i].w+EPS) {
            dist[v] = dist[u]+graph[i].w;
            if(!visit[v] && spfa_dfs(v)) // 如果没被访问,但后面的点被访问两次,则是负环
                return true;
            if(visit[v])  // 一个点在一条路径上被更新两次,有负环
                return true;
        }
    }
    visit[u] = 0;
    return false;
}

bool check(double mid) {
    init();
    for(int i = 0; i < m; i++) {
        addEdge(u[i], v[i], (a[v[i]-1]-mid*w[i])*-1);
    }

    bool exist = false;
    for(int i = 1; i <= n && !exist; i++) {
        for(int i = 0; i <= n; i++) dist[i] = 0;
        memset(visit, 0, sizeof(visit));
        if(spfa_dfs(i)) exist = true;
    }
    if(!exist) return true;
    return false;
}

int main() {
    while(scanf("%d%d", &n, &m) != EOF) {
        for(int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
        }
        for(int i = 0; i < m; i++) {
            scanf("%d%d%d", &u[i], &v[i], &w[i]);
        }

        double l = 0;
        double r = INF;
        while(r-l > EPS) {
            double mid = (l+r)/2.0;
            if(check(mid)) 
                r = mid;
            else
                l = mid;
        }

        printf("%.2f\n", r);
    }

    return 0;
}
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值