0/1分数规划总结

前言

最近在搞什么树套树,博弈论,啥啥啥的,时间实在紧迫,就先拿 0/1 规划开刀。

0/1 分数规划是什么

实际上是一类问题。

顾名思义,0/1 即 对于 \(n\) 个物品,选择或者不选择。分数,即对于每个物体,有两个属性 \(a_i,b_i\),选出物品的价值就是 \(\dfrac{\sum a_i \times d_i}{\sum b_i \times d_i}\)。(\(d_i\) 表示第 \(i\) 个物体选没选)。规划,即对于其求一个最值,且需要保证选择了 \(k\) 个。

假设我们此时要求一个最大值。

显然,我们可以考虑一个二分,对于每一个物体,他的权值为 \(a_i-b_i \times mid\)。(\(mid\) 表示当前二分的答案)

可以通过以下恒等变形深入理解为何这样设权:

\[\begin{aligned} & \dfrac{\sum a_i \times d_i}{\sum b_i \times d_i} \ge mid \\ & \sum a_i \times d_i \ge mid \times \sum b_i \times d_i \\ & \sum a_i \times d_i - mid \times \sum b_i \times d_i \ge 0\\ & \sum (a_i-mid\times b_i) \times d_i \ge 0 \end{aligned} \]

非常容易想到,对于每个点的的权值从大到小排序,选择排完后的前 \(k\) 个,如果选出来的权值大于 \(0\),则返回 \(\text{true}\),否则 \(\text{false}\)

于是,我们就有了一份版子题目的代码:

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;
}

对于同类型的题目,往往是将分数进行变形,或者有了更复杂的约束条件,但大致思想都是二分,难点往往都是如何去检查。

总的来说,这种题目还是比较具有套路性。

一些变形

\(\text{1.Talent show}\)

这道题目较于版子,多的限制是你要让 \(\sum b_i\times d_i \ge k\)

非常容易想到一个背包,一个物体空间为 \(b_i\),价值为 \(a_i-mid\times b_i\)。但是由于你的限制不是刚好而是大于等于,所以你的背包需要做一些改进。

一个很好的解决方案是顺推,当你发现当前的重量加上新的物体重量大于等于 \(k\) 时,就把他看作 \(k\) 继续转移就行了。(因为你在 \(\ge k\) 的重量上继续选物品实际上和刚好选 \(k\) 的重量没有什么本质的区别。)

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;
}

\(\text{2.Best Group}\)

这题较上题的不同之处在于是在有约束条件下选 \(k\) 个得到分数的最大值。

所以考虑在树上背包。

定义 \(dp_{x,j}\) 表示已 \(x\) 为根的子树中,选择了 \(j\) 个。

  • 如果 \(x\) 不选,显然他的子树里面也没有一个可以选择,故 \(dp_{x,0}=0\)

  • 如果选择 \(x\),则对于每个儿子 \(y\),枚举要在里面选择多少个,即 \(dp_{x,j}= \max \{dp_{x,j},dp_{y,k}+dp_{x,j - k}\}\)

最后看 \(dp_{0,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;
}

\(\text{3.Smallest Circle}\)

题意即,在图中找一个环,使得环上边权之和除以节点个数最小,求这个最小平均值。

一样是先二分,将边权设为原来的减去 \(mid\),看能不能在图中找到负环,如果找到了,就返回真,否则假。(因为环上是 \(n\) 个点,\(n\) 条边,所以一条边就可以对应一个 \(mid\)

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;
}

\(\text{4.Send Gift}\)

题意很清楚,这里不阐述了。

首先有二分答案。

有一点非常显然,即我们总是希望最大最小值在端点处。因为如果不在端点处,那么必然可以缩小这个区间,使得限制没有那么苛刻。

我们可以枚举所有的右端点,然后用单调队列维护在左边的最优值(维护两次,一次最小,一次最大)

但这样还不够,我们还要同理的在维护一遍所有的左端点。

但有些时候,由于你的区间长度在一个范围之内,就会导致你无法做到最大最小全在端点处,所以还需要维护一下 \(\text{rmq}\),来计算答案。

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;
}

后记

总的来说,这种题目本身并不难,难点往往是与其他知识点的迁移。

posted @ 2024-03-02 19:29  Saltyfish6  阅读(71)  评论(0编辑  收藏  举报
Document