参考:
http://www.cnblogs.com/perseawe/archive/2012/05/03/01fsgh.html
胡伯涛:《最小割模型在信息学竞赛中的应用》(强力推荐)
分数规划是一类问题。而01分数规划是分数规划的一个特例。
分数规划的一般形式: λ=f(x)=a(x)b(x),(x∈S) ,求 λ 最大或者最小。其中,解向量 x 在解空间 S 内, a(x)与b(x) 都是连续的实值函数。
01分数规划,就是解空间是 {0,1} 的分数规划问题,其实就是对一个条件,取还是不取进行选择。
这里以最小化问题进行解释。
设 λ∗ 为最优解。
λ∗=f(x∗)=a(x∗)b(x∗),(x∈S)
=>λ∗∗b(x∗)=a(x∗)
=>0=a(x∗)−λ∗∗b(x∗)
由上面的形式可以构造一个新函数 g(λ) :
g(λ)=minx∈S{a(x)−λ∗b(x)}
对之所以取小做出解释:
λ∗=f(x∗)=a(x∗)b(x∗)⩽a(x)b(x)
=>a(x)−λ∗∗b(x)⩾0
当 x=x∗ 时取最优解,且为0,所以,应该取小。
可以验证,无论是最小化问题还是最大化问题, g(λ) 的单调性都为单调递减。
找出分数规划不等式关系,写出 g(λ) 函数,尤其注意确定取大还是取小,二分枚举 λ ,验证答案,验证答案的策略一般是求 g(λ) 与0的接近程度,利用单调性逐步接近答案;验证答案的方法,根据题目类型,应用不同的算法,下面题解提供了几类。
还有一种方法,Dinkelbach。
求01分数规划的另一个方法就是Dinkelbach算法,他就是基于这样的一个思想,他并不会去二分答案,而是先随便给定一个答案,然后根据更优的解不断移动答案,逼近最优解。由于他对每次判定使用的更加充分,所以它比二分会快上很多。但是,他的弊端就是需要保存这个解,而我们知道,有时候验证一个解和求得一个解的复杂度是不同的。二分和Dinkelbach算法写法都非常简单,各有长处,大家要根据题目谨慎使用。
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
#include
#include
#include
#include
#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
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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_queuevector
, greater
> 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
#include
#include
#include
#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;
}