[BZOJ4819]-[Sdoi2017]新生舞会-01分数规划+费用流

说在前面

第一次写01分规,原来这么简单的嘛qwq


题目

BZOJ4819传送门

题目大意

有N个有标号的白球和黑球,现在需要将这些球黑白两两配对。如果将球(i,j)配对,就会获得协和值a[i][j]和违和值b[i][j](这是两种属性)。现在询问一种配对方式,使得下式中的C最大。其中C是获得的协和值之和 与 违和值之和 的比值

C=aij[i,j]bij[i,j] C = ∑ a i j ∗ [ i , j 配 对 ] ∑ b i j ∗ [ i , j 配 对 ]

输入输出格式

输入格式:
第一行一个整数N,含义如题
接下来N行,每行N个整数,表示a[i][j]
接下来N行,每行N个整数,表示b[i][j]

输出格式:
输出答案,保留六位小数


解法

关于01分规

这其实是一个很显然的01分数规划…
大概是这样的,要让这个式子 C=ab C = ∑ a ∑ b 中的 C C 最大,其中a和b都是要么同时选并获得其值,要么同时不选。
假设目前已经取到了最优的C,那么就应该是这样:Cab
把这个式子移项,得到 aCb0 ∑ a − C ∗ ∑ b ≤ 0 ,发现这个式子是满足二分性的。什么意思呢?我们现在随便选一个数,把他当作C带入式子,经过计算求出 aCb ∑ a − C ∗ ∑ b 的最大值,如果为正,那么说明C还可以变大,否则C需要变小,二分答案即可。当然,计算最大值这个步骤依题而定,比如这道题就用的费用流。

但是上面的二分并没有充分的利用所有信息。既然已经计算出 aCnowb ∑ a − C n o w ∗ ∑ b 的值,那么不妨就令 Cnext C n e x t 满足在当前决策情况下的 aCnextb=0 ∑ a − C n e x t ∗ ∑ b = 0 ,多搞几次就能逼近最优解,效率比直接二分要高很多。不过,这种迭代只适用于可以较简单的求出 a ∑ a 以及 b ∑ b 的情形(一个很难求的例子:求一张图里的 costlen c o s t l e n 最大的环)。因此,用二分或是迭代都需要视情况而定,哪个简单写哪个

对于这道题

那么对于这道题呢,先随便给C定一个值,然后开始迭代。
确定C值之后就是一个最大费用最大流,S向每个白球连边,边权1费用0,白球和黑球连边,边权1费用a[i][j]-C*b[i][j],每个黑球向T连边,边权1费用0。
枚举每条边的流量来判断a[i][j]和b[i][j]有没有被选,从而算出 a ∑ a b ∑ b 。当 Cnext C n e x t Cnow C n o w 的差值小于某个值的时候就终止


下面是自带大常数的代码

#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

const double eps = 5 * 1e-8 ;
int N , a[105][105] , b[105][105] , tp , head[205] ;
int girl[105] , boy[105] , S , T , id_c ;
struct Path{
    int pre , to , flow ;
    double fee ;
}p[20005+1000] ;

int dcmp( double x ){
    if( x > -eps && x < eps ) return 0 ;
    return x > 0 ? 1 : -1 ;
}

void In( int t1 , int t2 , int t3 , double t4 ){
    p[++tp] = ( Path ){ head[t1] , t2 , t3 , t4 } ; head[t1] = tp ;
    p[++tp] = ( Path ){ head[t2] , t1 , 0 , -t4 } ; head[t2] = tp ;
}

deque<int> que ;
double dis[205] ;
bool inque[205] ;
int pre[205] , preE[205] ;
bool SPFA(){
    for( int i = 1 ; i <= id_c ; i ++ ) dis[i] = -1e10 ;
    memset( pre + 1 , 0 , id_c * sizeof( int ) ) ;
    dis[S] = 0 , que.push_back( S ) , inque[S] = true ;

    while( !que.empty() ){
        int u = que.front() ;
        que.pop_front() ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to ;
            if( p[i].flow && dis[v] + 1e-10 < dis[u] + p[i].fee ){
                dis[v] = dis[u] + p[i].fee ;
                pre[v] = u , preE[v] = i ;
                if( !inque[v] ){
                    if( !que.empty() && dis[v] > dis[ que.front() ] + 1e-10 )
                        que.push_front( v ) ;
                    else que.push_back( v ) ;
                    inque[v] = true ;
                }
            }
        }
    } return pre[T] ;
}

void addFlow(){
    int now = T ;
    while( now != S ){
        p[ preE[now] ].flow -= 1 ;
        p[ preE[now]^1 ].flow += 1 ;
        now = pre[now] ;
    }
}

void check( double x ){
    tp = 1 ;
    memset( head + 1 , 0 , id_c * sizeof( int ) ) ;
    for( int i = 1 ; i <= N ; i ++ ){
        In( S , girl[i] , 1 , 0 ) ;
        In( boy[i] , T , 1 , 0 ) ;
    }
    for( int i = 1 ; i <= N ; i ++ )
        for( int j = 1 ; j <= N ; j ++ )
            In( girl[i] , boy[j] , 1 , 1.0 * a[i][j] - x * b[i][j] ) ;

    while( SPFA() ) addFlow() ;
}

void solve(){
    double ans , now = 1 ;
    do{
        ans = now ;
        check( ans ) ;
        double up = 0 , down = 0 ;
        for( int i = 1 ; i <= N ; i ++ )
            for( int j = head[ girl[i] ] , tmp = N ; j ; j = p[j].pre , tmp -- )
                if( !p[j].flow ){
                    up += a[i][tmp] , down += b[i][tmp] ;
                    break ;
                }
        now = up / down ;
    }while( dcmp( ans - now ) ) ;
    printf( "%.6f" , ans ) ;
}

int main(){
    scanf( "%d" , &N ) ;
    for( int i = 1 ; i <= N ; i ++ )
        girl[i] = ++id_c , boy[i] = ++id_c ;
    S = ++id_c , T = ++id_c ;
    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] ) ;
    solve() ;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值