数据结构与算法:01分数规划

前言

暂时还不知道这玩意儿有什么用()

一、内容

首先,01分数规划的基本问题是,给定 n 个数据,每个数据有 (a,b) 两个非负数值,要求从其中选出 k 个数据,使得 最大。

这里,因为比值形式必然不可能做到绝对正确,所以就考虑用二分答案去逼近这个最优解。那么假如当前二分的答案是 x,就有 ,将分母乘过去就有 ,移项匹配就有 ,此时称每一项为结余,所有项的累加和为结余和。那么问题就转化为,求出使得结余和尽可能接近 0 的 x 值。那么对于当前答案 x,考虑选 k 个数据,使得结余和尽可能大。那么若答案大于等于 0,就说明此时每条数据减少了,所以就可以去更大的答案中二分,否则就去更小的答案二分即可。

二、题目

1.Dropping Test

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const ld eps=1e-7;

void solve()
{
    while(true)
    {
        int n,k;
        cin>>n>>k;
        if(n==0&&k==0)
        {
            return ;
        }

        k=n-k;
        vector<vector<ld>>a(n+1,vector<ld>(2));
        for(int i=1;i<=n;i++)
        {
            //a b
            cin>>a[i][0];
        }
        for(int i=1;i<=n;i++)
        {
            cin>>a[i][1];
        }
        
        auto check=[&](ld m)->bool
        {
            vector<ld>val(n+1);
            for(int i=1;i<=n;i++)
            {
                val[i]=a[i][0]-m*a[i][1];
            }

            sort(val.begin()+1,val.end(),greater<>());

            ld sum=0;
            for(int i=1;i<=k;i++)
            {
                sum+=val[i];
            }
            return sum>=0;
        };

        ld l=0;
        ld r=0;
        for(int i=1;i<=n;i++)
        {
            r+=a[i][0];
        }
        ld m;
        ld ans=0;
        while(r-l>=eps)
        {
            m=(l+r)/2;
            if(check(m))
            {
                ans=m;
                l=m;
            }
            else
            {
                r=m;
            }
        }
        cout<<(int)(100*(ans+0.005))<<endl;
    }
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

就是板子题,需要注意的就是小数向上取整的方法,就是加上 0.005 之后再强转 int。

2.Talent Show G

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const double eps=1e-7;

void solve()
{
    int n,w;
    cin>>n>>w;
    vector<vector<int>>a(n+1,vector<int>(2));
    for(int i=1;i<=n;i++)
    {
        //重量 才艺
        cin>>a[i][0]>>a[i][1];
    }

    auto calc=[&](int i,double x)->double
    {
        return a[i][1]-x*a[i][0];
    };

    auto check=[&](double m)->bool
    {
        vector<vector<double>>dp(n+1,vector<double>(w+1,-INF));

        dp[0][0]=0;

        for(int i=1;i<=n;i++)
        {
            for(int p=0;p<=w;p++)
            {
                dp[i][p]=max(dp[i][p],dp[i-1][p]);

                int j=p+a[i][0];
                if(j>=w)
                {
                    dp[i][w]=max(dp[i][w],dp[i-1][p]+calc(i,m));
                }
                else
                {
                    dp[i][j]=max(dp[i][j],dp[i-1][p]+calc(i,m));
                }
            }
        }
        return dp[n][w]>=0;
    };

    double l=0;
    double r=1e6;
    double m;
    double ans=0;
    while(r-l>=eps)
    {
        m=(l+r)/2;
        if(check(m))
        {
            ans=m;
            l=m;
        }
        else
        {
            r=m;
        }
    }
    cout<<(int)(1000*ans)<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

对于这种比值问题,首先肯定是用二分答案去逼近最优解。那么在 check 的时候,问题就转化为了所有牛自由选择,使得总重量大于等于 w 的情况下,结余和尽可能大。

这个问题可以考虑用背包 dp 解决,就是定义 dp[i][j] 为 1~i 范围上选,重量严格等于 j 的最大结余和,然后每次讨论要或不要即可。又因为所有牛的重量和已经到了 1e8 的规模,所以肯定需要想办法进行优化。那么就可以考虑让 dp[i][w] 表示重量大于等于 w 的最大结余和,这样就把空间压缩下来了。

3.最小圈

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const double eps=1e-9;

void solve()
{
    int n,m;
    cin>>n>>m;
    vector<vector<pair<int ,double>>>g(n+1);
    int u,v;
    double w;
    for(int i=1;i<=m;i++)
    {
        cin>>u>>v>>w;
        g[u].push_back({v,w});
    }

    vector<int>path(n+1);
    vector<double>value(n+1);

    auto dfs=[&](auto &&self,int u,double x)->bool
    {
        //虚拟源点
        if(u==0)
        {
            for(int i=1;i<=n;i++)
            {
                if(self(self,i,x))
                {
                    return true;
                }
            }
            return false;
        }

        path[u]=1;
        for(auto [v,w]:g[u])
        {
            w-=x;

            if(value[v]>value[u]+w)
            {
                value[v]=value[u]+w;
                if(path[v]||self(self,v,x))
                {
                    return true;
                }
            }
        }
        path[u]=0;
        return false;
    };

    auto check=[&](double mid)->bool
    {
        path.assign(n+1,0);
        value.assign(n+1,0);

        return dfs(dfs,0,mid);
    };

    double l=-1e7;
    double r=1e7;
    double mid;
    double ans;
    while(r-l>=eps)
    {
        mid=(l+r)/2;
        if(check(mid))
        {
            r=mid;
        }
        else
        {
            ans=mid;
            l=mid;
        }
    }
    cout<<fixed<<setprecision(8)<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

可以发现,这个比值实际上就是边权的平均值。那么对于平均值,很常见的 trick 就是二分答案,然后 check 的时候把每个数都减去当前 mid 再去讨论。

那么首先,对于所有环来说,若其中的最小平均值为 min,那么若给每条边都减去 min 的权值,即给每个环的平均值都加上 abs(min),那么此时就不存在负环了。而若让所有边都减去一个大于 min 的权值,那么是无法把原来的负环消掉的。所以可以发现,若让所有边都减去 x 的权值,若此时还存在负环,就说明 x 大于最小平均值,所以就可以基于这个二分了。

之后,由于需要每次判断负环,这个就是 spfa 的优势区间了,这里提供一种递归写法,时间复杂度还是 O(n*m)。方法就是每次维护当前递归经过的路径,每次考察当前点的所有边。若能把去往点的最小权值更新得更小,那就更新然后递归,否则就不递归。所以若递归着递归着发现去往点之前来过,就说明找到了一个环,且经历这一圈后权值还能变得更小,那么就说明找到了负环。

感觉还是队列实现比较好懂()

4.最佳团体

byd 之前学的全还给左神了()

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

/*   /\_/\
*   (= ._.)
*   / >  \>
*/

/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/

#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define endl '\n'
typedef long long ll;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};

const double eps=1e-8;

void solve()
{
    int k,n;
    cin>>k>>n;
    vector<int>cost(n+1);
    vector<int>val(n+1);
    vector<vector<int>>g(n+1);
    for(int i=1,fa;i<=n;i++)
    {
        cin>>cost[i]>>val[i]>>fa;
        g[fa].push_back(i);
    }

    vector<int>dfn(n+2);
    vector<int>sz(n+2);
    int cnt=1;

    auto dfs=[&](auto &&self,int u)->void
    {
        int i=cnt++;
        dfn[u]=i;
        sz[i]=1;

        for(auto v:g[u])
        {
            self(self,v);

            sz[i]+=sz[dfn[v]];
        }
    };

    dfs(dfs,0);

    auto check=[&](double x)->bool
    {
        vector<double>dfv(n+2);
        for(int i=0;i<=n;i++)
        {
            dfv[dfn[i]]=val[i]-x*cost[i];
        }

        vector<vector<double>>dp(n+3,vector<double>(k+1));
        for(int j=1;j<=k;j++)
        {
            dp[n+2][j]=-INF;
        }

        for(int i=n+1;i>=1;i--)
        {
            for(int j=1;j<=k;j++)
            {
                dp[i][j]=max(dp[i+sz[i]][j],dfv[i]+dp[i+1][j-1]);
            }
        }
        return dp[2][k]>=0;
    };

    double l=0;
    double r=1e8;
    double m;
    double ans;
    while(r-l>=eps)
    {
        m=(l+r)/2;
        if(check(m))
        {
            ans=m;
            l=m;
        }
        else
        {
            r=m;
        }
    }
    cout<<fixed<<setprecision(3)<<ans<<endl;
}

void init()
{
}

signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    int t=1;
    //cin>>t;
    init();
    while(t--)
    {
        solve();    
    }
    return 0;
}

题目说得很复杂,其实就是要求选 k 个节点且这些节点必须跟 0 号节点处在同一个连通块内。之后,二分答案时还是每次考虑每个点的结余,然后选择结余和最大的方案,若大于等于 0 就去右侧二分。那么对于 check 方法,就是一个树上背包问题了。

简单提一下,就是借助 dfn 序的性质进行跨子树讨论。因为对于 dfn 序,子树中所有节点的 dfn 序必然比当前节点的 dfn 序大。那么若不要当前节点,其一整棵子树就都不能要了。而若要了当前节点,就去 dfn 序更大的部分讨论。由于是 dfn 序,所以更大的部分的深度必然大于等于当前节点,那么此时就天然保证是有效结构。最后,还是需要注意,因为根节点是 0,在 dfn 序里是 1,所以整棵树一共有 n+1 个节点。又因为 0 节点必须要,所以最终答案是 dp[2][k]。

总结

再接再厉!!加油!!

END

相关推荐
七七肆十九2 小时前
PTA 习题9-1 时间换算
c语言·算法
XW01059992 小时前
5-6统计工龄
数据结构·python·算法
行稳方能走远2 小时前
结构体传参,到底该传值还是传指针?
c++·单片机
sycmancia2 小时前
C++——函数模板的概念和意义
c++
EQUINOX12 小时前
倍增优化dp,P10976 统计重复个数
算法·数学建模·动态规划
闻缺陷则喜何志丹2 小时前
【巴什博弈 线性筛】P8901 [USACO22DEC] Circular Barn S|普及+
c++·数学·洛谷·巴什博弈·线型筛
样例过了就是过了2 小时前
LeetCode热题100 电话号码的字母组合
数据结构·c++·算法·leetcode·dfs
nervermore9902 小时前
1.10 面试经典150题-多数元素
算法
c++逐梦人2 小时前
二分查找模版及二分答案例题
算法·蓝桥杯