01分數規划入門


01分數規划,簡單的來說,就是有一些二元組(si,pi),從中選取一些二元組,使得∑si / ∑pi最大(最小)。

這種題一類通用的解法就是,我們假設x = ∑si / ∑pi的最大(小)值,那么就有x * ∑pi = ∑si ,即∑s - x * ∑pi= 0。也就是說,當某一個值x滿足上述式子的時候,它就是要求的值。我們可以想到枚舉……不過再想想,這個可以二分答案。

所以我們直接二分答案,當上述式子>0,說明答案小了,<0則說明答案大了,這樣計算即可。

這是一種解決問題的方法,具體應該怎么做我們要看題來分析。

01分數規划有這樣幾種基本的題型(當然還有很多別的……暫時不在juruo的考慮范圍內)

1.01分數規划

2.最優比率生成樹

3.最優比率生成環

(當然還有什么最優比率最小割等等……不在juruo當前研究范圍之內)

1.01分數規划。

基礎題:poj2976

這個就是一開始說的這么一碼事……選取n-k(原題是不選k)個物品使得比率最大。

我們考慮對上面的式子進行轉化,之后我們只要給每個物品重新賦值為a - x*b,排序之后直接取前n-k大,判斷這個值的正負性,然后按上面情況二分即可。

如果不大明白為什么是這么取得的,可以看這里(個人覺得很詳細)

附上代碼。

 

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 10005;
const int INF = 1000000009;
const double eps = 1e-6;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

struct test
{
   double a,b,d;
   bool operator < (const test &g) const
   {
      return d > g.d;
   }
}t[1005];

int n,k;
double L,R,ans,mid;

bool pd(double x)
{
   double cur = 0;
   rep(i,1,n) t[i].d = t[i].a - x * t[i].b;
   sort(t+1,t+1+n);
   rep(i,1,n-k) cur += t[i].d;
   return cur >= 0;
}

int main()
{
   while(1)
   {
      n = read(),k = read();
      if(!n && !k) break;
      rep(i,1,n) t[i].a = read();
      rep(i,1,n) t[i].b = read();
      L = 0,R = 1e10;
      while(fabs(R - L) > eps)
      {
     mid = (L + R) / 2.0;
     if(pd(mid)) L = mid;
     else R = mid;
      }
      printf("%.0lf\n",(mid * 100.0));
   }
   return 0;
}
View Code

 

 

2.最優比率生成樹。

基礎題:poj2728

簡單描述就是,每一條路有一個花費p和一個權值s,要在圖上選取一些邊構成一棵生成樹,求生成樹的∑pi / ∑si最小值。

我們還是考慮套用上面的模板,其實這個和01分數規划很像的。只要對每條邊重新賦一個值,因為這次我們要求的是花費比上權值,所以和上一道題反了過來,賦值是p-x * s。之后因為要求最小,直接求最小生成樹即可。判斷一下最小生成樹的權值和,如果小於0說明答案設大了,否則說明答案設小了。

這道題是完全圖,所以我們需要用prim求最小生成樹。(突然發現我從來都是用kruskal都沒用過prim),但是它的想法其實很簡單,我們從任意一個點開始,之后選取一個最近的點,並且用這個點更新其他點的距離,之后再尋找一個更近的點,再去更新距離。它的復雜度是O(n2)的。

看一下代碼。

 

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 1000005;
const int N = 1005;
const int INF = 1000000009;
const double eps = 1e-6;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

struct node
{
   double x,y,z;
}a[N];

int n;
double L,R,mid,dis[1005][1005],hb[1005][1005],map[1005][1005],cur[1005];
bool vis[1005];

double calc(int p,int q)
{
   return sqrt((a[p].x - a[q].x) * (a[p].x - a[q].x) + (a[p].y - a[q].y) * (a[p].y - a[q].y));
}

void init()
{
   rep(i,1,n) scanf("%lf %lf %lf",&a[i].x,&a[i].y,&a[i].z);
   rep(i,1,n)
   rep(j,i+1,n) dis[i][j] = dis[j][i] = calc(i,j),hb[i][j] = hb[j][i] = fabs(a[i].z - a[j].z);
}

double prim()
{
   double ans = 0;
   rep(i,1,n) vis[i] = 0,cur[i] = map[1][i];
   vis[1] = 1;
   rep(i,1,n-1)
   {
      double minn = INF;
      int pos = 0;
      rep(j,1,n) if(!vis[j] && minn > cur[j]) pos = j,minn = cur[j];
      if(minn == INF) break;
      vis[pos] = 1;
      ans += minn;
      rep(j,1,n) if(!vis[j]) cur[j] = min(cur[j],map[pos][j]);
   }
   return ans;
}

bool pd(double x)
{
   rep(i,1,n)
   rep(j,i+1,n) map[i][j] = map[j][i] = hb[i][j] - x * dis[i][j];
   return prim() < 0;
}

int main()
{
   while(1)
   {
      n = read();
      if(!n) break;
      init();
      L = 0,R = 1e7;
      while(fabs(R-L) > eps)
      {
     mid = (L+R) / 2.0;
     if(pd(mid)) R = mid;
     else L = mid;
      }
      printf("%.3lf\n",L);
   }
   return 0;
}
/*
4
0 0 0
0 1 1
1 1 2
1 0 3
4
0 0 0
0 1 1
1 1 2
1 0 3
0

 */
View Code

 

3.最優比率生成環。

基本的思想還是一樣的,就是對式子進行一下變形。不過這里稍微有一些不同。

最大化的時候,假設最大值為r*,那么就有∑pi / ∑si <= r*,變形之后就有∑si * r* - ∑pi >= 0.

最小化的時候,假設最小值為r*,那么就有∑pi / ∑si >= r*,變形之后就有∑pi - ∑si * r* >= 0.

這個地方很容易給人搞懵……因為其實我們很容易在求值的時候只使用一種變形式子的方法,然后改變二分的方向,不過這樣的話計算的結果會出錯。

因為這里要判斷的不大一樣,如果求最大值的時候設小了,那么就會在原圖中出現負環。我們把每條邊按上述方法重新賦值,之后我們使用基於dfs的spfa去判負環,之后改變二分的值即可。

如果求最小值,也按上述方法賦值,重新計算即可。

例題:天路 觀光奶牛

這兩道題都是要求最大比率的環,直接按照上述方法求就可以了。

看一下代碼。(觀光奶牛)

 

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 10005;
const int N = 7005;
const int INF = 1000000009;
const double eps = 1e-6;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

struct edge
{
   int to,next;
   double v,t;
}e[M];

int l,p,x,y,ecnt,head[N];
double f[N],z,L,R,mid,dis[N];
bool vis[N];

void add(int x,int y,double z,double a)
{
   e[++ecnt].to = y;
   e[ecnt].v = a;
   e[ecnt].t = z;
   e[ecnt].next = head[x];
   head[x] = ecnt;
}

bool pd(double x,int pos)
{
   vis[pos] = 1;
   for(int i = head[pos];i;i = e[i].next)
   {
      if(dis[e[i].to] > dis[pos] + e[i].t * x - e[i].v)
      {
     if(vis[e[i].to]) return 1;
     dis[e[i].to] = dis[pos] + e[i].t * x - e[i].v;
     if(pd(x,e[i].to)) return 1;
      }
   }
   vis[pos] = 0;
   return 0;
}

int main()
{
   l = read(),p = read();
   rep(i,1,l) scanf("%lf",&f[i]);
   rep(i,1,p) x = read(),y = read(),scanf("%lf",&z),add(x,y,z,f[y]);
   rep(i,1,l) add(0,i,0,0);
   L = 0,R = 1e7;
   while(fabs(R-L) > eps)
   {
      mid = (L+R) / 2.0;
      memset(dis,0x3f,sizeof(dis)),memset(vis,0,sizeof(vis)),dis[0] = 0;
      if(pd(mid,0)) L = mid;
      else R = mid;
   }
   printf("%.2lf\n",L);
   return 0;
}
View Code

 

 

之后還有一道題,01分數規划+樹形背包!

傳送門

其實這個題好像說難也不難,因為很好想到怎么做,就是01分數規划的常用套路+樹形背包。

我們對於每個人重新進行賦值(其實好像大部分的題都可以用分子-分母*進行賦值……)之后直接跑樹形背包,求一下跑出來的最大值,然后改變二分的大小。

這題嚴重卡常,首先你肯定要用dfs序的樹背包,不過其實還是卡不過去,即使把二分上界改為4都不行……精度如果低的話還會wa掉,最后只好開了O2.

我們看一下代碼。

// luogu-judger-enable-o2
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 1000005;
const int N = 3005;
const int INF = 1000000009;
const double eps = 1e-6;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

struct edge
{
   int next,to;
}e[N<<1];

int k,n,head[N],ecnt,dfn[N],pos[N],idx,size[N],x;
double dp[N][N],s[N],p[N],L,R,mid,val[N];
bool vis[N][N];

void add(int x,int y)
{
   e[++ecnt].to = y;
   e[ecnt].next = head[x];
   head[x] = ecnt;
}

void dfs(int x)
{
   dfn[x] = ++idx,pos[idx] = x,size[x] = 1;
   for(int i = head[x];i;i = e[i].next)
   {
      dfs(e[i].to);
      size[x] += size[e[i].to];
   }
}

double DP()
{
   double maxn = -INF;
   memset(dp,-0x3f,sizeof(dp));
   vis[1][0] = 1,dp[1][0] = 0;
   rep(i,1,idx)
   {
      rep(j,0,min(i,k))
      {
     if(!vis[i][j]) continue;
     dp[i+1][j+1] = max(dp[i+1][j+1],dp[i][j] + val[pos[i]]);
     dp[i+size[pos[i]]][j] = max(dp[i][j],dp[i+size[pos[i]]][j]);
     vis[i+1][j+1] = vis[i+size[pos[i]]][j] = 1;
      }
   }
   rep(i,1,idx+1) maxn = max(maxn,dp[i][k+1]);
   return maxn;
}

bool pd(double x)
{
   memset(vis,0,sizeof(vis));
   rep(i,1,idx) val[i] = p[i] - x * s[i];
   return DP() > -eps;
}

int main()
{
   k = read(),n = read();
   rep(i,1,n) scanf("%lf %lf",&s[i],&p[i]),x = read(),add(x,i);
   dfs(0);
   L = 0,R = 1e6;
   while(fabs(R-L) > eps)
   {
      mid = (L+R) / 2.0;
      if(pd(mid)) L = mid;
      else R = mid;
   }
   printf("%.3lf\n",L);
   return 0;
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM