前言
暂时还不知道这玩意儿有什么用()
一、内容
首先,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]。
总结
再接再厉!!加油!!