P8392 Uplifting Excursion

D e s c r i p t i o n \mathrm{Description} Description

2 m + 1 2m+1 2m+1 种物品,重量分别为 − m , − m + 1 , … , m − 1 , m -m,-m+1,\ldots, m-1,m m,m+1,,m1,m。重量为 i i i 的物品有 a i a_i ai 个。

你需要拿走若干物品,使得这些物品重量之和恰好为 l l l。在此基础上,你需要拿尽可能多的物品。

问在物品重量之和恰好为 l l l 的基础上,你最多能拿多少物品。

S o l u t i o n \mathrm{Solution} Solution

B r u t e   F o r c e \mathrm{Brute\ Force} Brute Force(暴力)

m , a m,a m,a 比较小的时候, L L L 最大为 ∑ i = 1 m a i \sum\limits_{i=1}^ma_i i=1mai,那么可以考虑直接使用多重背包进行求解。

众所周知,多重背包有 2 2 2 种优化方式:二进制优化;单调队列

时间复杂度分别为 O ( n w log ⁡ c ) O(nw\log c) O(nwlogc) O ( n w ) O(nw) O(nw)。其中, n n n 为物品种数, w w w 为最大容量, c c c 为物品数量。

F a s t e r   W a y \mathrm{Faster\ Way} Faster Way

a a a 的数量级攀升至 1 0 12 10^{12} 1012 级别,那么 L L L 的范围就会非常大,故肯定要通过一些操作使得价值的范围缩小。

可以考虑贪心,想要使得范围缩小,且还要最优。一个直观的想法为将选择的重量之和的范围缩小至 [ l − m , l ] [l-m,l] [lm,l],因为 1 1 1 次最多加 m m m,所以显然可以控制在该范围内。

那么,如何做是最优的呢?

肯定要先把所有的负数加入,然后考虑单价,贪心选择的物品单价是由高到低的,所以计算一下单价:

  • 对于 i < 0 i<0 i<0,即重量为负值,由于已经选过,所以再选就等同于删掉,即物品重量会增加 − i -i i,数量会减少 1 1 1,故单价 = − 1 − i =-\frac{1}{-i} =i1
  • 对于 i > 0 i>0 i>0,即重量为正值,由于未曾选过,所以再选物品重量会增加 i i i,数量会增加 1 1 1,故单价 = 1 i =\frac{1}{i} =i1

那么,单价由高向低排列重量选择顺序为: 1 , 2 , … , m − 1 , m , − m , − m + 1 , … , − 1 1,2,\dots,m-1,m,-m,-m+1,\dots,-1 1,2,,m1,m,m,m+1,,1

这样,就可以计算出重量之和在 [ l − m , l ] [l-m,l] [lm,l] 之间的 1 1 1 种方案了~~~

int Sum = 0;
for (int i = 0; i <= M; i ++) //先将全部负数加入
    Sum += A[i] * (i - M), Cnt[i] = A[i]; //Cnt 维护用了多少个(具体为什么,后面会说)
for (int i = M + 1; i <= 2 * M; i ++) //按 1,2,...,m 的顺序依次考虑
{
    int num = min((L - Sum) / (i - M), A[i]); //如果再加 A[i] 个就超过 L 了,那么就加到 (L - Sum) / (i - M),即要使和小于等于 L,最多还能加多少
    Sum += num * (i - M), Cnt[i] = num;
}
for (int i = 0; i < M; i ++) //按 -m,-m+1,...,-1 的顺序依次考虑
{
    int num = min((L - Sum) / (M - i), A[i]); //与上文类似
    Sum += num * (M - i), Cnt[i] -= num;
}

大家可能会问了,这样还是有可能会加很多,然后再减回来呀?这样你背包容积不就又很大了吗?

确实是这样,不过这里有一个性质,后面背包调整到 l l l 的过程最多操作 2 m + 1 2m+1 2m+1 次。

P r o o f : \mathrm{Proof:} Proof:

一定有一种方法使得在调整途中一定是在 [ l − m , l + m ] [l-m,l+m] [lm,l+m] 之间的,因为 1 1 1 次最多加 m m m,或减 m m m,一定存在一种方式使得是在该区间 [ l − m , l + m ] [l-m,l+m] [lm,l+m] 的。

那么,最优调整途中会不会出现 2 2 2 次相同的重量,一定不会。

可以考虑反证法,假设重量之和在调整途中的变化为: W 1 → W 2 → W 3 → W 4 … W_1\rightarrow W_2\rightarrow W_3 \rightarrow W_4\dots W1W2W3W4

若, W 2 = W 4 W_2=W_4 W2=W4 了,那么考虑 2 → 4 2\rightarrow 4 24 途中如果 加入的数抛弃的数 多,那么由于是基于贪心的最优策略进行的 DP,所以不可能存在加入的数更多的情况,如果存在,贪心的时候就会加进去了,故该情况不成立。反之,如果 2 → 4 2\rightarrow 4 24 途中 加入的数抛弃的数 少,那么此时数的个数还不如 2 2 2 时刻的多,重量又没变,那还要 3 → 4 3\rightarrow 4 34 这个过程有什么用呢?

故,与假设矛盾,证毕


那么,背包 DP 的时候,对于每种物品,维护最多可以加入多少个,可以抛弃多少个,之后用二进制分组单调队列优化即可。

所以,存在一种调整最多操作 2 m + 1 2m+1 2m+1 次,所以最多加减 m 2 m^2 m2 个重量。所以背包的容积是 O ( m 2 ) O(m^2) O(m2) 级别的,物品种数是 O ( m ) O(m) O(m) 级别的,如果用单调队列优化多重背包的话,那么时间复杂度为 O ( m 3 ) O(m^3) O(m3),如果用二进制优化,会多个 log ⁡ \log log,都是可以通过本题的。

int Use = 0;
for (int i = 0; i <= 2 * M; i ++) //已经选择的数量
    Use += Cnt[i];

for (int i = 0; i <= 2 * M; i ++) //二进制分组
{
    if (i == M) continue;
    int k = 1, s = min(A[i] - Cnt[i], M * M); //还能再加入的数量
    while (k <= s)
    {
        w[ ++ idx] = (i - M) * k, v[idx] = k;
        s -= k, k *= 2;
    }
    if (s > 0) w[ ++ idx] = (i - M) * s, v[idx] = s;
    k = 1, s = min(Cnt[i], M * M); //可以抛弃的数量,也就是当前选择的数量
    while (k <= s)
    {
        w[ ++ idx] = -(i - M) * k, v[idx] = -k; //都要加符号,表示抛弃
        s -= k, k *= 2;
    }
    if (s > 0) w[ ++ idx] = -(i - M) * s, v[idx] = -s;
}
//由于空间不太够,所以采用滚动数组优化,注意不能只开 1 维,因为本题既有负的权值,又有正的,无法通过更改枚举顺序来进行滚动数组
memset(F, -0x3f, sizeof F);
F[0][B] = 0;
for (int i = 1; i <= idx; i ++)
    for (int j = B + M * M; j >= 0; j --)
    {
        F[i & 1][j] = F[(i - 1) & 1][j];
        if (j >= w[i] && j - w[i] <= B + M * M) F[i & 1][j] = max(F[i & 1][j], F[(i - 1) & 1][j - w[i]] + v[i]);
    }

if (F[idx & 1][B + L - Sum] + Use < -INF) puts("impossible");
else cout << F[idx & 1][B + L - Sum] + Use << endl;

A c c e p t e d   C o d e \mathrm{Accepted\ Code} Accepted Code

#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef pair<int, int> PII;
typedef long long LL;

const int SIZE = 3e2 + 10, B = 9e4, INF = 1e9;

int M, L;
int A[SIZE * 2], Cnt[SIZE * 2];
int F[2][SIZE * SIZE + B];
int w[SIZE * SIZE], v[SIZE * SIZE], idx;

signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	cin >> M >> L;

	for (int i = 0; i <= 2 * M; i ++)
		cin >> A[i];

	int Sum = 0;
	for (int i = 0; i <= M; i ++)
		Sum += A[i] * (i - M), Cnt[i] = A[i];
	for (int i = M + 1; i <= 2 * M; i ++)
	{
		int num = min((L - Sum) / (i - M), A[i]);
		Sum += num * (i - M), Cnt[i] = num;
	}
	for (int i = 0; i < M; i ++)
	{
		int num = min((L - Sum) / (M - i), A[i]);
		Sum += num * (M - i), Cnt[i] -= num;
	}

	if (L - Sum > M)
	{
		puts("impossible");
		return 0;
	}

	int Use = 0;
	for (int i = 0; i <= 2 * M; i ++)
		Use += Cnt[i];

	for (int i = 0; i <= 2 * M; i ++)
	{
		if (i == M) continue;
		int k = 1, s = min(A[i] - Cnt[i], M * M);
		while (k <= s)
		{
			w[ ++ idx] = (i - M) * k, v[idx] = k;
			s -= k, k *= 2;
		}
		if (s > 0) w[ ++ idx] = (i - M) * s, v[idx] = s;
		k = 1, s = min(Cnt[i], M * M);
		while (k <= s)
		{
			w[ ++ idx] = -(i - M) * k, v[idx] = -k;
			s -= k, k *= 2;
		}
		if (s > 0) w[ ++ idx] = -(i - M) * s, v[idx] = -s;
	}

	memset(F, -0x3f, sizeof F);
	F[0][B] = 0;
	for (int i = 1; i <= idx; i ++)
		for (int j = B + M * M; j >= 0; j --)
		{
			F[i & 1][j] = F[(i - 1) & 1][j];
			if (j >= w[i] && j - w[i] <= B + M * M) F[i & 1][j] = max(F[i & 1][j], F[(i - 1) & 1][j - w[i]] + v[i]);
		}

	if (F[idx & 1][B + L - Sum] + Use < -INF) puts("impossible");
	else cout << F[idx & 1][B + L - Sum] + Use << endl;

	return 0;
}

更多推荐