0/1分数规划总结
前言
最近在搞什么树套树,博弈论,啥啥啥的,时间实在紧迫,就先拿 0/1 规划开刀。
0/1 分数规划是什么
实际上是一类问题。
顾名思义,0/1 即 对于
假设我们此时要求一个最大值。
显然,我们可以考虑一个二分,对于每一个物体,他的权值为
可以通过以下恒等变形深入理解为何这样设权:
非常容易想到,对于每个点的的权值从大到小排序,选择排完后的前
于是,我们就有了一份版子题目的代码:
bool check(double mid)
{
for (int i = 1; i <= n; ++i) c[i] = a[i] - mid * b[i];
stable_sort(c + 1, c + n + 1);
reverse(c + 1, c + n + 1);
double now = 0;
for (int i = 1; i <= k; ++i) now += c[i];
return now >= 0;
}
对于同类型的题目,往往是将分数进行变形,或者有了更复杂的约束条件,但大致思想都是二分,难点往往都是如何去检查。
总的来说,这种题目还是比较具有套路性。
一些变形
这道题目较于版子,多的限制是你要让
非常容易想到一个背包,一个物体空间为
一个很好的解决方案是顺推,当你发现当前的重量加上新的物体重量大于等于
bool check(double mid)
{
for (int i = 1; i <= n; ++i) c[i] = b[i] - mid * a[i];
for (int i = 0; i <= n; ++i) for (int j = 0; j <= k; ++j) dp[i][j] = -1e9;
dp[0][0] = 0;
for (int i = 0; i < n; ++i)
{
for (int j = 0; j <= k; ++j)
{
dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);
dp[i + 1][min(j + a[i + 1], k)] = max(dp[i + 1][min(j + a[i + 1], k)], dp[i][j] + c[i + 1]);
}
}
return dp[n][k] >= 0;
}
这题较上题的不同之处在于是在有约束条件下选
所以考虑在树上背包。
定义
-
如果
不选,显然他的子树里面也没有一个可以选择,故 。 -
如果选择
,则对于每个儿子 ,枚举要在里面选择多少个,即
最后看
至于时间复杂度,可以看这篇博客。
int dfs(int x)
{
// cout << x << endl;
for (int i = 0; i <= m + 1; ++i) dp[x][i] = -1000000000;
// dp[x][0] = 0;
if(x) dp[x][1] = 1.0 * fight[x] - cost[x] * MId;
int now = 1;
if(!x) dp[x][1] = 0;
for (int i = fst[x]; i; i = arr[i].nxt)
{
int j = arr[i].tar, L;
now += L = dfs(j);
for (int k = min(now, m + 1); k >= 1; --k)
{
for (int l = 0; l <= min(k - 1, L); ++l)
{
dp[x][k] = max(dp[x][k - l] + dp[j][l], dp[x][k]);
}
}
}
return now;
}
bool check(double mid)
{
MId = mid;
dfs(0);
if(dp[0][m + 1] >= 0) return true;
else return false;
}
题意即,在图中找一个环,使得环上边权之和除以节点个数最小,求这个最小平均值。
一样是先二分,将边权设为原来的减去
bool spfa(int st)
{
vis[st] = true;
for (int i = fst[st]; i; i = arr[i].nxt)
{
int j = arr[i].tar;
double k = arr[i].num;
if(dis[j] > dis[st] + k)
{
dis[j] = dis[st] + k;
if(vis[j]) return true;
if(spfa(j)) return true;
}
}
vis[st] = false;
return false;
}
bool check(double mid)
{
for (int i = 1; i <= cnt; ++i) arr[i].num -= mid;
for (int i = 1; i <= n; ++i) dis[i] = 0;
memset(vis, 0, sizeof(vis));
bool flg = 0;
for (int i = 1; i <= n; ++i)
{
if(spfa(i))
{
flg = 1;
break;
}
}
for (int i = 1; i <= cnt; ++i) arr[i].num += mid;
return flg;
}
题意很清楚,这里不阐述了。
首先有二分答案。
有一点非常显然,即我们总是希望最大最小值在端点处。因为如果不在端点处,那么必然可以缩小这个区间,使得限制没有那么苛刻。
我们可以枚举所有的右端点,然后用单调队列维护在左边的最优值(维护两次,一次最小,一次最大)
但这样还不够,我们还要同理的在维护一遍所有的左端点。
但有些时候,由于你的区间长度在一个范围之内,就会导致你无法做到最大最小全在端点处,所以还需要维护一下
int dp[100005][21][2];
void init()
{
for (int i = 1; i <= n; ++i) dp[i][0][0] = dp[i][0][1] = a[i];
for (int j = 1; (1 << j) <= n; ++j)
{
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
{
dp[i][j][0] = min(dp[i][j - 1][0], dp[(i + (1 << j - 1))][j - 1][0]);
dp[i][j][1] = max(dp[i][j - 1][1], dp[(i + (1 << j - 1))][j - 1][1]);
}
}
}
int rmq(int l, int r, int op)
{
int k = __lg(r - l + 1);
if(op) return max(dp[l][k][op], dp[r - (1 << k) + 1][k][op]);
else return min(dp[l][k][op], dp[r - (1 << k) + 1][k][op]);
}
bool check(double mid)
{
deque<int> p;
for (int i = l; i <= n; ++i)
{
while(!p.empty() && i - p.front() + 1 > r) p.pop_front();
while(!p.empty() && (a[p.back()] + (n - p.back()) * mid <= a[i - l + 1] + (n - (i - l + 1)) * mid)) p.pop_back();
p.push_back(i - l + 1);
if((rmq(p.front(), i, 1) - rmq(p.front(), i, 0)) >= (i - p.front() + m) * 1.0 * mid) return true;
}
while(!p.empty()) p.pop_back();
for (int i = n - l + 1; i >= 1; --i)
{
while(!p.empty() && p.front() - i + 1 > r) p.pop_front();
while(!p.empty() && (a[p.back()] + p.back() * mid <= a[i + l - 1] + (i + l - 1) * mid)) p.pop_back();
p.push_back(i + l - 1);
if(rmq(i, p.front(), 1) - rmq(i, p.front(), 0) >= (p.front() - i + m) * 1.0 * mid) return true;
}
while(!p.empty()) p.pop_back();
for (int i = n - l + 1; i >= 1; --i)
{
while(!p.empty() && p.front() - i + 1 > r) p.pop_front();
while(!p.empty() && (a[p.back()] + p.back() * mid >= a[i + l - 1] + (i + l - 1) * mid)) p.pop_back();
p.push_back(i + l - 1);
if(rmq(i, p.front(), 1) - rmq(i, p.front(), 0) >= (p.front() - i + m) * 1.0 * mid) return true;
}
while(!p.empty()) p.pop_back();
for (int i = l; i <= n; ++i)
{
while(!p.empty() && i - p.front() + 1 > r) p.pop_front();
while(!p.empty() && (a[p.back()] + (n - p.back()) * mid >= a[i - l + 1] + (n - (i - l + 1)) * mid)) p.pop_back();
p.push_back(i - l + 1);
if((rmq(p.front(), i, 1) - rmq(p.front(), i, 0)) >= (i - p.front() + m) * 1.0 * mid) return true;
}
return false;
}
后记
总的来说,这种题目本身并不难,难点往往是与其他知识点的迁移。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】