• 【Learning】分数规划


    分数规划

      
    ​  分数规划是一类决策性问题。一般地,题目会要求你针对问题规划一种方案,使得其代价函数最小或最大。其中,代价函数一般是分数形式,且分子分母的构成元素一般呈现一一对应关系。
      
      直接上例题观察:BZOJ2402
      
      img
      
      分数规划的重要思路是二分答案。我们记答案为(c),则问题转变为判断是否存在((i,j))使得

    [frac{y_i+q_j}{x_i+p_j}ge c\ y_i+q_jge c(x_i+p_j)\ (y_i-c*x_i)+(q_j-c*p_j)ge0 ]

    ​  化简后我们发现,原本分别处于分子和分母的元素有了一一对应的关系,使得每一类元素之间在表达式上互不干扰。只要知道此式有没有可能成立,就能判断(c)是否合法。这也是分数规划的一个重要突破点。
      
    ​  所以显然是要分别找到(a)(b)的路径上两个点(i,j),使得((y_i-c*x_i))((q_j-c*p_j))分别取最大值。如果两者加起来大于等于0,说明(c)这个答案合法,因此我们提升二分下界;否则(c)答案不合法,我们降低二分上界。
      
      取最大值的这些操作,可以用树剖+上凸包回答。
      
      总的时间复杂度是(O(nlog^4n)),但是能过!
      

    Code

      

    #include <cstdio>
    #include <vector>
    #include <algorithm>
    #define mp make_pair
    #define pb push_back
    using namespace std;
    typedef pair<double,double> pdd;
    const int N=30005;
    const double EPS=1e-6;
    int n;
    double a[N][4];
    int h[N],tot,dep[N],pre[N][16],size[N],son[N];
    struct Edge{int v,next;}e[N*2];
    int who[N],loc[N],locnt,top[N];
    inline void addEdge(int u,int v){
    	e[++tot]=(Edge){v,h[u]}; h[u]=tot;
    	e[++tot]=(Edge){u,h[v]}; h[v]=tot;
    }
    void dfs1(int u,int fa){
    	size[u]=1; son[u]=0;
    	dep[u]=dep[fa]+1;
    	pre[u][0]=fa;
    	for(int i=1;i<=15;i++) pre[u][i]=pre[pre[u][i-1]][i-1];
    	for(int i=h[u],v;i;i=e[i].next)
    		if((v=e[i].v)!=fa){
    			dfs1(v,u);
    			size[u]+=size[v];
    			if(!son[u]||size[v]>size[son[u]]) son[u]=v;
    		}
    }
    void dfs2(int u,int _top){
    	top[u]=_top;
    	who[loc[u]=++locnt]=u;
    	if(!son[u]) return;
    	dfs2(son[u],_top);
    	for(int i=h[u],v;i;i=e[i].next)
    		if((v=e[i].v)!=pre[u][0]&&v!=son[u])
    			dfs2(v,v);
    }
    inline void merge(pdd &u,pdd v){
    	if(v.first>u.first) u.first=v.first;
    	if(v.second>u.second) u.second=v.second;
    }
    inline double getk(pdd u,pdd v){return (v.second-u.second)/(v.first-u.first);}
    struct Hull{
    	vector<pdd> a;
    	void insert(double x,double y){a.pb(mp(x,y));}
    	void build(){
    		sort(a.begin(),a.end());
    		int top=0;
    		vector<pdd> st;
    		for(int i=0,sz=a.size();i<sz;i++){
    			while(top>1&&getk(st[top-2],st[top-1])<getk(st[top-1],a[i]))
    				top--,st.pop_back();
    			st.pb(a[i]); top++;
    		}
    		a=st;
    		a.resize(top);
    	}
    	double query(double k){
    		int l=0,r=a.size()-2,mid;
    		while(l<=r){
    			mid=(l+r)>>1;	
    			if(getk(a[mid],a[mid+1])-EPS<=k) 
    				r=mid-1;
    			else l=mid+1;
    		}
    		return a[l].second-k*a[l].first;
    	}
    };
    namespace SEG{
    	int rt,sz,ch[N*2][2];
    	Hull s[N*2][2];
    	void build(int &u,int l,int r){
    		u=++sz;
    		for(int i=l;i<=r;i++){
    			int x=who[i];
    			s[u][0].insert(a[x][0],a[x][1]);
    			s[u][1].insert(a[x][2],a[x][3]);
    		}
    		s[u][0].build(); s[u][1].build();
    		if(l==r) return;
    		int mid=(l+r)>>1;
    		build(ch[u][0],l,mid);
    		build(ch[u][1],mid+1,r);
    	}
    	pdd query(int u,int l,int r,int L,int R,double k){
    		if(L<=l&&r<=R)
    			return mp(s[u][0].query(k),s[u][1].query(k));
    		pdd res=mp(-1e10,-1e10);	
    		int mid=(l+r)>>1;
    		if(L<=mid) merge(res,query(ch[u][0],l,mid,L,R,k));
    		if(mid<R) merge(res,query(ch[u][1],mid+1,r,L,R,k));
    		return res;
    	}
    }
    int getLCA(int u,int v){
    	if(dep[u]<dep[v]) swap(u,v);
    	for(int i=15;i>=0;i--)
    		if(dep[pre[u][i]]>=dep[v]) u=pre[u][i];
    	if(u==v) return u;
    	for(int i=15;i>=0;i--)
    		if(pre[u][i]!=pre[v][i]) u=pre[u][i],v=pre[v][i];
    	return pre[u][0];
    }
    bool judge(int u,int v,double k){
    	pdd res=mp(-1e10,-1e10);	
    	int lca=getLCA(u,v);		
    	for(;dep[top[u]]>=dep[lca];u=pre[top[u]][0])
    		merge(res,SEG::query(SEG::rt,1,n,loc[top[u]],loc[u],k));
    	if(dep[u]>=dep[lca])
    		merge(res,SEG::query(SEG::rt,1,n,loc[lca],loc[u],k));
    	for(;dep[top[v]]>=dep[lca];v=pre[top[v]][0])
    		merge(res,SEG::query(SEG::rt,1,n,loc[top[v]],loc[v],k));
    	if(dep[v]>=dep[lca])
    		merge(res,SEG::query(SEG::rt,1,n,loc[lca],loc[v],k));
    	return res.first+res.second+EPS>=0;	
    }
    int main(){
    	scanf("%d",&n);
    	for(int i=0;i<4;i++)
    		for(int j=1;j<=n;j++) scanf("%lf",&a[j][i]);
    	for(int i=1,u,v;i<n;i++){
    		scanf("%d%d",&u,&v);
    		addEdge(u,v);
    	}
    	dfs1(1,0);
    	dfs2(1,1);
    	SEG::build(SEG::rt,1,n);
    	int m,u,v;
    	scanf("%d",&m);
    	while(m--){
    		scanf("%d%d",&u,&v);
    		double l=0,r=100001,mid,eps=5e-4;
    		while(l+eps<r){
    			mid=(l+r)*0.5;
    			if(judge(u,v,mid)) l=mid;
    			else r=mid;
    		}
    		printf("%.5lf
    ",l);
    	}
    	return 0;
    }
    

      
        
      

    例题

      

    【BZOJ4819】【SDOI2017】新生舞会

      
    ​  传送门
      
      按照分数规划的基本思想,我们二分答案(c),化简题目中的代价函数式,转化成一个判定问题:

    [frac{x_1+x_2+...+x_n}{y_1+y_2+...+y_n}ge c\ (x_1-c*y_1)+(x_2-c*y_2)+...+(x_n-c*y_n)ge 0 ]

      其中(x_i)表示第(i)个男生和他所选女生的喜悦程度,(y_i)表示对应的不协调程度。
      
    ​  我们发现虽然各个括号在表达式上互不相关,但是从题意的角度来看:不能有多个男生同时选定一个女生。
      
    ​  但是我们的目的还是要将这(n)个括号的和最大化,并和0比较,从而判定答案(c)是否合法。
      
    ​  匹配,带权,最大化——我们想到了最大带权匹配模型。可以从最大费用最大流的角度来建图:源点向所有男生连((1,0))的边;所有男生向所有女生连边,边权为相应的括号,流量为1;所有女生向汇点连((1,0))的边。
      
    ​  那么我们跑一次费用流就可以得出一种方案,使得括号之和最大。此时和0判断即可。当然用KM会快很多。
       

    Code

       

    #include <cstdio>
    #include <queue>
    using namespace std;
    const int N=105,INF=1e9;
    const double EPS=1e-6;
    int n,a[N][N],b[N][N];
    int h[N*2],tot;
    struct Edge{int v,f;double c;int next;}e[(N*N+2*N)*2];
    int S,T;
    double dis[N*2];
    int pree[N*2],preu[N*2];
    bool inq[N*2];
    queue<int> q;
    inline void addEdge(int u,int v,int f,double c){
        e[++tot]=(Edge){v,f,c,h[u]}; h[u]=tot;
        e[++tot]=(Edge){u,0,-c,h[v]}; h[v]=tot;
    }
    bool spfa(){
        for(int i=1;i<=T;i++)
            dis[i]=-INF,inq[i]=false;
        while(!q.empty()) q.pop();
        q.push(S);
        dis[S]=0; inq[S]=true;
        while(!q.empty()){
            int u=q.front(); q.pop();
            inq[u]=false;
            for(int i=h[u],v;i;i=e[i].next)
                if(e[i].f&&dis[v=e[i].v]<dis[u]+e[i].c){
                    dis[v]=dis[u]+e[i].c;
                    preu[v]=u; pree[v]=i;
                    if(!inq[v]){
                        inq[v]=true;
                        q.push(v);
                    }
                }
        }
        return dis[T]>-INF;
    }
    double maxcostflow(){
        double res=0;
        while(spfa()){
            int flow=INF;
            for(int u=T;u!=S;u=preu[u])
                flow=min(flow,e[pree[u]].f);
            res+=dis[T]*flow;
            for(int u=T;u!=S;u=preu[u]){
                e[pree[u]].f-=flow;
                e[pree[u]^1].f+=flow;
            }
        }
        return res;
    }
    bool judge(double c){
        S=n*2+1; T=S+1;
        for(int i=1;i<=T;i++) h[i]=0;
        tot=1;
        for(int i=1;i<=n;i++)
            addEdge(S,i,1,0),addEdge(n+i,T,1,0);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                addEdge(i,n+j,1,a[i][j]-c*b[i][j]);
        return maxcostflow()+EPS>=0;
    }
    int main(){
        scanf("%d",&n);
        for(int i=1;i<=n;i++) 
            for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
        for(int i=1;i<=n;i++) 
            for(int j=1;j<=n;j++) scanf("%d",&b[i][j]);
        double l=0,r=10000,mid;
        while(l+(5e-8)<r){
            mid=(l+r)*0.5;
            if(judge(mid)) l=mid;
            else r=mid;
        }
        printf("%.6lf
    ",l);
        return 0;
    }
    

      
    ​  
      
    ​  
      

    【BZOJ1690】【Usaco2007 Dec】奶牛的旅行

      
      传送门(权限题)

      按照套路二分答案展开化简,得到一个用于判定(c)是否合法的式子:

    [(a_1-c*b_1)+(a_2-c*b_2)+...+(a_{m-1}-c*b_{m-1})ge 0 ]

      其中选择的路径有(m)个点,(a_i)表示第(i)个点的乐趣值,(b_i)表示第(i)个点走到第(i+1)个点的距离。由于第(m)个点和第1个点相同,价值又不重复计算,所以(a_m)不需要算进答案,式子变得十分规律好看。
      
      那么我们把每一条边((u,v,w))的边权设置为(a_u-c*w)。如果图中存在正环,那么选择这一个环游览就能满足上述式子。
      
      所以用spfa来判断每次二分判定时构建的图是否存在正环即可。
       

    Code

      

    #include <cstdio>
    #include <queue>
    using namespace std;
    const int N=1005,M=5005,INF=1e9;
    const double EPS=1e-6;
    int n,m,a[N],inp[M][3];
    int h[N],tot;
    struct Edge{int v;double w;int next;}e[M];
    inline void addEdge(int u,int v,double w){e[++tot]=(Edge){v,w,h[u]}; h[u]=tot;}
    double dis[N];
    bool inq[N];
    int qt[N];
    queue<int> q;
    bool spfa(){
        while(!q.empty()) q.pop();
        for(int i=1;i<=n;i++) dis[i]=0,inq[i]=true,qt[i]=1,q.push(i);
        while(!q.empty()){
            int u=q.front(); q.pop();
            inq[u]=false;
            for(int i=h[u],v;i;i=e[i].next)
                if(dis[v=e[i].v]+EPS>dis[u]+e[i].w){
                    dis[v]=dis[u]+e[i].w;
                    if(!inq[v]){
                        inq[v]=true;
                        qt[v]++;
                        if(qt[v]>n) return true;
                        q.push(v);
                    }
                }
        }
        return false;
    }
    bool judge(double c){
        tot=0;
        for(int i=1;i<=n;i++) h[i]=0;
        for(int i=1;i<=m;i++)
            addEdge(inp[i][0],inp[i][1],-(a[inp[i][0]]-c*inp[i][2]));
        return spfa();
    }
    int main(){
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++) scanf("%d",&a[i]);
        for(int i=1;i<=m;i++) scanf("%d%d%d",inp[i],inp[i]+1,inp[i]+2);
        double l=0,r=1000,mid;
        while(l+(1e-3)<r){
            mid=(l+r)*0.5;
            if(judge(mid)) l=mid;
            else r=mid;
        }
        printf("%.2lf
    ",l);
        return 0;
    }
    

      
      
      
      
      

    【BZOJ4898】【Apio2017】商旅

      
    ​  传送门
      
    ​  此题和上题非常类似。但是题目的“环”好像没有强调是简单环,只需要走一圈回到出发点即可。
      
    ​  首先用Floyd计算出原图上两两点之间的距离(w_{u,v})
      
    ​  再考虑购买商品的过程。题目所谓的“从A地买一个商品,拿着走向B地卖掉,利润为x”,可以简化为从A向B连一条权值为x的边。显然从A走向B总是应该选利润最大的那个商品走,所以A向B只需要连一条边。
      
    ​  当然,为了满足题目意思,对于原图的每一条边我们应该相应地连一条两端一样,边权为0的边,以适应空手旅行的情况。
      
    ​  原问题变成:在新构建的图上,找到一个环,使得代价函数最大。
      
      惯例化简代价函数,得到判定式

    [(x_1-c*w_1)+(x_2-c*w_2)+...+(x_{m-1}-c*w_{m-1})ge 0 ]

      (x_i)表示第(i)个点走向第(i+1)个点的边权(也就是这么走的利润),(w_i)表示从(i)走到(i+1)的距离(和上一题几乎一模一样)。
      
    ​  所以再次用spfa判正环是否存在即可。
      
      注意题目要求输出整数,我们用整数进行二分,可以提高效率。
      

    Code

      

    #include <cstdio>
    using namespace std;
    const int N=105,M=9905,K=1005,INF=1e9;
    const double EPS=1e-6;
    int n,m,kk,a[N],w[N][N];
    int sell[N][K],buy[N][K];
    int cnt,inp[N*N*2][3];
    int h[N],tot;
    struct Edge{int v;double w;int next;}e[N*N*2];
    inline void addEdge(int u,int v,double w){e[++tot]=(Edge){v,w,h[u]}; h[u]=tot;}
    double dis[N];
    bool inq[N];
    inline int min(int x,int y){return x<y?x:y;}
    inline int max(int x,int y){return x>y?x:y;}
    bool dfs(int u){
        inq[u]=true;
        for(int i=h[u],v;i;i=e[i].next){
            v=e[i].v;
            if(dis[u]+e[i].w<dis[v]+EPS){
                dis[v]=dis[u]+e[i].w;
                if(inq[v]) return true;
                else if(dfs(v)) return true;
            }
        }
        inq[u]=false;
        return false;
    }
    bool judge(double c){
        tot=0;
        for(int i=1;i<=n;i++) h[i]=0;
        for(int i=1;i<=cnt;i++)
            addEdge(inp[i][0],inp[i][1],-(inp[i][2]-c*w[inp[i][0]][inp[i][1]]));
        for(int i=1;i<=n;i++) dis[i]=0,inq[i]=false;
        for(int i=1;i<=n;i++) 
            if(dfs(i)) return true;
        return false;
    }
    int main(){
        scanf("%d%d%d",&n,&m,&kk);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=kk;j++) scanf("%d%d",&buy[i][j],&sell[i][j]);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++) w[i][j]=INF;
        for(int i=1,u,v,c;i<=m;i++){
            scanf("%d%d%d",&u,&v,&c);
            w[u][v]=min(w[u][v],c);
            cnt++;
            inp[cnt][0]=u; inp[cnt][1]=v; inp[cnt][2]=0;
        }
        for(int k=1;k<=n;k++)
            for(int i=1;i<=n;i++)
                for(int j=1;j<=n;j++)
                    w[i][j]=min(w[i][j],w[i][k]+w[k][j]);
        for(int u=1;u<=n;u++)
            for(int v=1;v<=n;v++)
                if(w[u][v]!=INF){
                    int best=0;
                    for(int i=1;i<=kk;i++)
                        if(buy[u][i]!=-1&&sell[v][i]!=-1)
                            best=max(best,sell[v][i]-buy[u][i]);
                    if(best){
                        cnt++;
                        inp[cnt][0]=u; inp[cnt][1]=v; inp[cnt][2]=best;
                    }
                }
        int l=0,r=1e9,mid;
        while(l<=r){
            mid=(l+r)>>1;
            if(judge(mid)) l=mid+1;
            else r=mid-1;
        }
        printf("%d
    ",r);
        return 0;
    }
    

      
      
      
        
      

    细节注意

      
      题目要求的精度非常关键,不要因为二分的控制部分不精确,使得答案出现偏差。
      
      一定要记得使用(eps)!比如在spfa判定是否要松弛的地方要加上eps。

  • 相关阅读:
    MQTT的编译和安装(mosquitto)
    四、固件分析-固件逆向
    三、固件分析-固件提取
    二、硬件分析-电路分析
    一、硬件分析
    gdb调试
    AES加密中遇到的坑
    多线程中快速定位段错误位置
    理想的数据加密流程
    Centos进入单用户修改root密码
  • 原文地址:https://www.cnblogs.com/RogerDTZ/p/9240768.html
Copyright © 2020-2023  润新知