<think>题目要求:在n个物品中选择m个,使得任意两个物品的位置差至少为k,并最大化受欢迎程度之和。
现有代码分析:
使用动态规划,定义dp[i][j]表示前i个物品中选择了j个,且最后一个选择的物品是第i个时的最大和。
初始化:dp[i][1] = a[i],因为只选一个时,就是该物品的受欢迎程度。
转移方程:对于每个i(从k+1到n),考虑j从1到i-k(这里j表示之前最后一个选择的物品位置),然后对于l表示当前已经选择的个数(从0开始,最多到j-1位置能选的数量),如果l+1<=m,则更新dp[i][l+1] = max(dp[i][l+1], dp[j][l] + a[i])。
但是,这个三重循环的时间复杂度是O(n^2*m),而n最大为10000,m最大100,但最坏情况下n^2*m=10000*10000*100=10^10,会超时。
优化思路:
我们需要优化最内层的循环。观察转移方程:
dp[i][j] = max{ dp[t][j-1] + a[i] } 其中 t 需要满足 t <= i-k (因为相邻两个物品位置差至少k,所以上一个物品的位置最多到i-k)
也就是说,对于固定的j(当前所选物品个数),我们需要在区间[1, i-k]中找到一个最大的dp[t][j-1]来更新dp[i][j]。
因此,我们可以用一个辅助数组来记录每个j对应的区间最大值。具体地:
设f[i][j] = dp[i][j],那么状态转移方程变为:
dp[i][j] = max_{t from max(1,?) to i-k} { dp[t][j-1] } + a[i]
这里t的范围是:[1, i-k](注意:当i-k<1时,说明没有前一个物品可选,此时无法选多个)
但是注意,题目要求任意两个物品的位置差至少为k,所以上一个物品的位置t必须满足i-t>=k,即t<=i-k。
因此,我们可以用另一个数组g[j]来记录到当前位置i之前,对于j-1这个状态,在区间[1, i-k]中的最大值。但是注意,i是递增的,我们可以一边遍历一边更新。
具体优化:
我们改变循环顺序,先枚举选择的个数j(从1到m),再枚举位置i(从1到n)。这样,对于固定的j,我们可以在遍历i的过程中维护一个区间最大值。然而,由于区间是[1, i-k](即从1到i-k),我们可以用一个数组preMax来记录到每个位置i为止的前缀最大值?但是注意,这里区间是固定的左边界1,变动的右边界i-k,所以我们可以用前缀最大值。但是,注意不是整个前缀,因为i-k可能很小,而且我们要求的是区间[1, i-k]的最大值。
然而,还有一点:我们更新dp[i][j]时,需要的是j-1状态在区间[1, i-k]的最大值。那么,我们可以在遍历i的过程中,当i>=k+1时,区间[1, i-k]的最大值可以通过在遍历过程中不断加入新的点(即dp[i-k][j-1])并维护一个最大值来得到吗?注意,i是递增的,但区间右端点i-k也是递增的,所以我们可以用单调队列或者直接维护一个变量来记录最大值?但是注意,我们每次需要的是[1, i-k]的最大值,而这个区间每次右端点增加1(当i增加1时,右端点增加1)。所以我们可以维护一个最大值变量,每次当i增加1时,我们将新的dp[i-k][j-1]加入考虑,然后更新最大值。
具体步骤:
对于每个状态j(从1到m):
初始化一个最大值变量max_val为负无穷(因为我们可能有负数)。
然后遍历i(从1到n):
如果当前i>=k+1,那么我们就可以将dp[i-k][j-1]纳入最大值范围(因为此时i-k>=1,且这个位置的值已经计算好了),所以更新max_val = max(max_val, dp[i-k][j-1])。
然后,如果i<=k,那么我们无法从前面选择一个物品(因为前面没有物品满足位置差>=k),所以当j>1时,dp[i][j]只能为负无穷(表示不可达);当j=1时,dp[i][1]就是a[i](初始化已经做了)。注意,我们这里在循环内,所以对于j>=2的情况:
dp[i][j] = max_val + a[i] (前提是max_val不为负无穷,否则说明无法选出j-1个物品)
但是注意:当i<k+1时,max_val还没有更新(因为i-k<=0,没有加入任何值),所以max_val还是负无穷,那么dp[i][j]就会是负无穷,这是合理的(因为无法选出两个物品且满足位置差>=k)。
但是,这里有一个问题:我们每次更新dp[i][j]时,max_val维护的是区间[1, i-k]中dp[t][j-1]的最大值。然而,在遍历i的过程中,当i从1开始增大到k+1时,我们才开始加入dp[1][j-1](当i=k+1时,i-k=1,此时加入dp[1][j-1])。然后i增大到k+2时,加入dp[2][j-1],并更新最大值。这样,当计算dp[i][j]时,max_val就是[1, i-k]的最大值。
然而,还有一点:我们可能不需要从1开始,因为第一个物品的位置只能是1,所以没有更小的位置。因此,这个方法可行。
注意:初始条件dp[i][1]=a[i]已经设置好。
但是,这样我们只优化了内层循环,将内层循环优化为O(1),总时间复杂度变为O(n*m),因为外层是m,内层是n,所以100*10000=1e6,可以接受。
因此,我们可以重新设计动态规划的状态和转移:
定义dp[i][j]:前i个物品中,选择了j个,且最后一个物品是第i个时的最大和。
转移方程:
当j=1时,dp[i][1] = a[i]
当j>1时,dp[i][j] = max_{t from 1 to i-k} { dp[t][j-1] } + a[i]
然后,我们用一个二维数组dp,以及一个辅助数组(或者一个变量)来维护最大值。
但是,注意我们只需要前一列(即j-1)的最大值。所以我们可以用两个一维数组:dp_prev[i]表示上一列(即j-1状态)中dp[i][j-1]的值。然后对于当前列j,我们遍历i,同时维护一个最大值max_val,表示从位置1到位置i-k的dp_prev的最大值。
具体步骤:
初始化:对于每个i,dp_current[i] = a[i] // 即j=1的情况
然后,对于j从2到m:
max_val = -1e9 // 初始化为负无穷
for i from 1 to n:
// 当i>=k+1时,我们可以将i-k位置的值加入最大值(注意:这里的位置i-k对应的是上一列的值)
if i-k>=1:
max_val = max(max_val, dp_prev[i-k]) // 注意:dp_prev[i-k]表示位置i-k在上一列的值
// 然后更新当前列j的dp[i][j]
if i < j: // 注意:不可能在i个物品中选出多于i个的物品,所以dp[i][j]应该为负无穷(或者跳过),但实际上j从2开始,i至少为j才能选j个且最后一个为i?所以当i<j时,不可能选j个,所以dp_current[i] = -1e9
dp_current[i] = -1e9
else:
// 但是,当我们要求j个物品,最后一个在位置i,那么前面j-1个物品至少需要j-1个位置,且最后一个位置要在i-k之前。所以当i-k>=1且max_val不为负无穷时,可以更新。
if max_val == -1e9: // 说明前面没有足够的位置
dp_current[i] = -1e9
else:
dp_current[i] = max_val + a[i]
// 然后,将dp_current赋值给dp_prev,以便下一轮迭代(即j+1状态使用)
dp_prev = dp_current // 注意:这里dp_prev是一个一维数组,表示上一列(即j-1状态)的值。而当前列j更新完毕后,就变成了j状态,然后下一轮j+1时,j状态就变成了新的上一列。
最后,我们要求的是所有满足条件的dp[i][m](最后一个物品为i,且i>=m)的最大值。注意:题目要求任意两个物品位置差>=k,所以最后一个物品位置至少是m+(m-1)*(k-1)?不,题目只要求相邻两个物品位置差>=k,但不要求连续。所以最后一个物品位置可以是大于等于m的位置?但是注意,我们选m个物品,最后一个位置至少是m(最小值是第一个物品在1,第二个在1+k,第三个在1+2k,...,最后一个在1+(m-1)*k)。所以我们在所有i从(m-1)*k+1到n中找最大值?不对,因为位置差不一定都是k,可以更大。所以最后一个位置i至少大于等于m,但更精确的下界是:1+(m-1)*k <= i <= n。因此,我们在所有i上取最大值即可(只要dp[i][m]不是负无穷)。
但是,注意我们循环中当i< (j-1)*k+1 时,可能无法选出j个物品(因为第一个物品最小位置1,第二个最小位置1+k,...第j个最小位置1+(j-1)*k)。所以当i < (j-1)*k+1时,dp_current[i]应该是负无穷(不可达)。但是我们的循环中,在j固定时,i从1开始,所以当i< (j-1)*k+1时,我们不会使用前面的值(因为i-k会小于1,所以max_val还是负无穷,因此dp_current[i]自然就是负无穷)。所以不用特别处理。
然而,我们还需要注意:当k=1时,位置差至少1,即可以连续选。所以最小位置就是j(即选j个物品,最后一个位置最小为j)。
因此,在循环中,我们只需要按照上述方法更新即可。
但是,还有一点:在j=2时,我们要求dp[i][2] = max_{t from 1 to i-k} dp_prev[t] + a[i]。而dp_prev[t]就是dp[t][1](即a[t])。所以我们需要在i>=k+1时,维护一个从1到i-k的a[t]的最大值。然后加上a[i]得到dp[i][2]。
但是,在j=3时,dp_prev[t]是dp[t][2]的值,然后我们需要在i>=2k+1时(因为t<=i-k,而我们要求t至少为1,所以i-k>=1,但为了有前两个物品,第一个物品位置p,第二个位置p+k,第三个位置>=p+2k,所以第三个位置最小为1+2k,因此当i>=2k+1时,我们才可能有值),维护一个从1到i-k的dp_prev的最大值。
因此,在循环中,我们对于每个j,从2到m,然后遍历i从1到n。在遍历i时,当i>=k+1时,我们就将dp_prev[i-k]加入到max_val的更新中。注意,这里i-k是递增的,所以我们只需要依次加入即可。
但是,为什么加入的是dp_prev[i-k]?因为我们要求的是[1, i-k]的最大值,而随着i增加,i-k也在增加,所以新的值就是dp_prev[i-k]。我们用一个变量max_val来保存,每次加入dp_prev[i-k]并更新max_val,那么当前的max_val就是[1, i-k]的最大值。
然而,这个max_val是否包含所有1到i-k的值?是的,因为i从小到大,每次加入的位置就是i-k(当i=k+1时,加入位置1;i=k+2时,加入位置2;...;当i增加1,加入的位置就是i-k)。所以遍历到i时,我们已经加入了所有位置从1到i-k的值(因为i-k之前的位置都已经在之前的循环中加入过了)。
因此,我们只需要维护一个变量max_val,在每次i>=k+1时,更新max_val = max(max_val, dp_prev[i-k])。
但是,当j=2时,dp_prev[i-k]就是dp[i-k][1]=a[i-k]。然后我们更新dp_current[i] = max_val + a[i]。
但是,这里有一个边界:当i<k+1时,即i<=k,那么max_val不会被更新(因为i-k<=0,没有位置),所以max_val保持负无穷,那么dp_current[i]就是负无穷(表示不可达),这是正确的(因为选2个物品,第一个物品位置t和第二个位置i,要求i-t>=k,但是i<=k,那么t<=i-k<=0,不存在,所以不可达)。
所以,我们可以这样实现:
初始化一个一维数组dp(大小为n+1),表示j=1的状态:dp[i] = a[i](i从1到n)
然后,初始化一个数组prev = dp // 上一列的值
对于j从2到m:
初始化max_val = -∞
初始化一个数组curr(大小为n+1),初始值全部为-∞(因为可能无法达到)
对于i从1到n:
如果i>=k+1:
max_val = max(max_val, prev[i-k]) // 注意:这里我们加入的位置是i-k(相对于当前i,上一个物品的位置可以是i-k,并且这个位置的值是prev[i-k])
如果当前i>=j: // 因为选j个物品,至少需要j个位置(且最后一个位置为i,则至少需要从1到i中有j个物品,所以i必须>=j)。但是,即使i>=j,也不一定能选出j个(因为位置差限制),所以还要看max_val
if max_val != -∞:
curr[i] = max_val + a[i]
否则(即i<j):
curr[i] = -∞ // 或者不更新(因为初始已经是-∞)
然后,将prev更新为curr,进入下一轮循环(j+1)
最后,遍历所有位置i(从1到n),取prev[i]的最大值,就是答案。注意:我们最后的状态是j=m,所以prev就是j=m的dp数组。
但是,注意:我们要求的是任意两个物品的位置差都大于等于k,而我们的状态设计保证了相邻两个物品(即最后两个)位置差>=k(因为上一个位置在t<=i-k,所以i-t>=k)。但是,前面的物品之间呢?实际上,由于我们每次转移时,都要求前j-1个物品满足条件(且最后一个位置是t),然后加上一个位置i,且i-t>=k,所以整个序列的相邻物品位置差都>=k。因为序列的相邻位置差是由多个这样的相邻转移构成的(状态中已经保证了前j-1个物品满足条件,而新加入的与上一个位置差>=k)。
因此,这个动态规划是正确的。
但是,还有一点:有可能存在非相邻的物品位置差小于k吗?不会,因为序列是递增的(位置从小到大),所以任意两个物品的位置差等于中间相邻物品位置差之和。例如,三个物品位置为p1, p2, p3,且p2-p1>=k, p3-p2>=k,那么p3-p1>=2k>=k(因为k>=1)。所以只要相邻的位置差都>=k,那么任意两个物品的位置差都>=k。
因此,我们只需要保证相邻物品位置差>=k即可。
所以,优化后的代码如下:
注意:题目中物品位置从1到n。
另外,题目中要求任意两个物品位置差>=k,所以相邻两个物品位置差>=k即可。
代码实现:
1. 读入n, m, k 和一维数组a(大小为n+1,下标1~n)
2. 如果m==0,输出0(但题目保证m>=1?输入描述中m<=100,m>=1?因为m<=n,且n>=1,所以m至少为1?但题目说“买m个物品”,所以m>=1。但是,如果m=0,背包里0个物品,那么和就是0。所以处理m=0的情况?题目没有说m>=1。所以安全起见,考虑m=0的情况:输出0)
3. 初始化dp_prev数组(大小为n+1,初始化为负无穷),然后对于i从1到n,dp_prev[i]=a[i](即j=1)
4. 如果m==1,那么直接取dp_prev中的最大值(因为可以任意选一个,所以最大值就是a数组的最大值)
5. 否则,对于j从2到m:
初始化max_val = -1e9(负无穷大)
初始化dp_curr数组(大小为n+1,初始化为负无穷)
for i from 1 to n:
// 如果i-k>=1,说明可以加入新的候选值:dp_prev[i-k](因为位置i-k对应的值已经计算好了,且这个位置可以作为当前物品的上一个物品)
if i-k>=1:
if dp_prev[i-k] > max_val:
max_val = dp_prev[i-k]
// 注意:这里max_val可能还是负无穷(如果还没有更新过),说明没有合适的前一个物品。
// 然后,如果当前i>=j(表示至少需要j个位置,而我们当前在位置i,所以如果i<j,那么不可能选j个物品,所以跳过;但是即使i>=j,也可能因为max_val为负无穷而无法更新)。
if i >= j:
if max_val != -1e9:
dp_curr[i] = max_val + a[i]
// 否则,dp_curr[i]保持负无穷(不可达)
// (如果i<j,则dp_curr[i]保持负无穷,因为初始化为负无穷)
然后,将dp_curr赋值给dp_prev,以便下一轮使用。
6. 最后,在dp_prev数组中(即j=m的状态)中,寻找最大值。注意:最后一个物品的位置i必须大于等于(m-1)*k+1(因为第一个物品位置至少1,第二个至少1+k,第三个至少1+2k,...,最后一个至少1+(m-1)*k),所以i的范围是从(m-1)*k+1到n。但是,我们在动态规划过程中已经确保了这一点吗?实际上,在j=m时,我们要求i>=m,并且我们只更新了i>=m的情况。但是,位置i必须至少为1+(m-1)*k,所以如果1+(m-1)*k大于n,那么无解?但题目保证一定有解。所以我们在所有i中取最大值(因为可能最后一个物品位置大于等于1+(m-1)*k,且小于等于n)。
7. 输出最大值。
但是,注意:可能有的状态无法达到,所以dp_prev[i]可能是负无穷,我们要跳过。
但是,我们初始化max_val为-1e9,更新dp_curr[i]时,如果max_val还是-1e9,那么dp_curr[i]就是负无穷。所以最后取最大值时,我们只需要在dp_prev数组中取最大值(忽略负无穷)。
但是,有可能整个dp_prev数组都是负无穷?那么说明无法选出m个物品?但是题目保证有解。
所以,我们只需取max_value = max(dp_prev[1..n]),然后输出max_value。
但是,注意:在j=2时,我们加入dp_prev[i-k](即j=1时的值),这些值都是a[i-k](可能是负数)。所以max_val可能是一个负数,然后加上a[i](也可能是负数)得到dp_curr[i]。所以不能因为负数而忽略。
因此,我们直接取最大值即可。
但是,我们初始化dp_curr数组为负无穷,所以如果没有更新,就保持负无穷。最后取max_value时,我们只关心非负无穷的值。
然而,题目可能全是负数,所以我们的负无穷要足够小(比如-1e18),但题目说答案在int范围内,所以我们可以用-1e18(如果int是32位,那么-1e9其实可能不够,因为最多100个负数,每个负数最小可能是-1e9,那么和最小-100e9=-1e11,所以负无穷可以设为-1e18?但是题目保证在int范围,所以我们可以用INT_MIN?注意:C++中INT_MIN是-2147483648,大约是-2e9。而题目中n最大10000,m最大100,负数最小可能是-1e9,那么最大负数和最小为-1e9*100=-1e11,所以INT_MIN不够。因此,我们使用long long?但是题目说答案在int范围内,所以最终答案不会超过int,但中间计算可能超过int?题目没有说,但备注说在int范围内,所以我们可以用int。但是,负无穷要足够小,我们可以用-0x3f3f3f3f(大约-1e9)?或者用-1e18,但是用int会溢出,所以建议用long long。或者,题目保证在int范围内,所以最大值不会超过int,但最小值可能达到-100e9(即-1e11),所以我们需要用long long来避免整数溢出?但是题目说答案在int范围,所以最终答案不会超过int,但是中间计算可能超过int?所以为了安全,使用long long。
根据输入描述:受欢迎程度可能是负数,最多100个,每个绝对值可能在10^9内?但题目说答案在int范围内,所以最大和不会超过int范围,但是中间计算dp时可能有累加,所以累加和可能超过int?所以使用long long更安全。
但是,题目没有给出受欢迎程度的绝对值范围,但n<=10000,m<=100,所以最大和最多100*10^9=1e12(如果每个都是10^9),但int最大是2e9左右,所以long long是必要的。
然而,题目说“答案保证在int范围内”,所以最终答案不会超过int,但中间计算可能超过?所以为了避免溢出,我们用long long存储中间状态,最后输出时转成int(但不会溢出,因为最终答案在int范围内)。或者,题目既然保证在int范围内,那我们就用int?但是中间计算可能超过int。所以最好用long long。
因此,我们使用long long来存储dp数组。
另外,我们还可以优化空间:因为dp_prev和dp_curr都是一维数组,所以空间为O(n),总空间O(n*m)?不,我们只用了两个一维数组,所以空间O(n)。
优化后的代码:
步骤:
long long max_val;
vector<long long> dp_prev(n+1, -1e18); // 初始化为负无穷
// 初始化j=1
for (int i=1; i<=n; i++) {
dp_prev[i] = a[i];
}
if (m==1) {
long long ans = *max_element(dp_prev.begin()+1, dp_prev.end());
cout << ans << endl;
return 0;
}
vector<long long> dp_curr(n+1, -1e18);
for (int j=2; j<=m; j++) {
max_val = -1e18; // 每一轮j开始,max_val重置
// 注意:对于同一个j,i从1到n,每次更新max_val
for (int i=1; i<=n; i++) {
// 如果i-k>=1,则更新max_val:加入dp_prev[i-k]
if (i-k>=1) {
if (dp_prev[i-k] > max_val) {
max_val = dp_prev[i-k];
}
}
// 注意:当i>=j时,才可能选出j个物品(因为物品位置不能重复,且必须递增),否则不可能
if (i>=j) {
// 如果max_val已经被更新过(即大于负无穷),则更新dp_curr[i]
if (max_val != -1e18) {
dp_curr[i] = max_val + a[i];
}
// 否则,保持负无穷(不可达)
}
// 如果i<j,那么dp_curr[i]保留负无穷(因为初始化为负无穷)
}
// 交换dp_prev和dp_curr,准备下一轮
dp_prev = dp_curr;
// 重置dp_curr为负无穷(准备下一轮使用,但下一轮我们会重新计算dp_curr,所以这里可以不清除,因为下一轮会被覆盖?或者我们每次在循环内重新赋值?但是下一轮开始时,我们要重新初始化dp_curr为负无穷)
// 所以,在循环开始(j循环)时,我们重新初始化dp_curr:在j循环内部初始化dp_curr?这里我们在j循环的开头重新定义dp_curr
// 因此,把dp_curr的初始化放在j循环内部
}
// 此时dp_prev是j=m的状态
long long ans = -1e18;
for (int i=1; i<=n; i++) {
if (dp_prev[i] > ans) {
ans = dp_prev[i];
}
}
cout << ans << endl;
但是,这里有一个问题:在同一个j循环中,我们更新dp_curr[i]时,max_val是到当前位置i为止,dp_prev[1]到dp_prev[i-k]的最大值。而dp_prev[i-k]是上一轮(j-1)在位置i-k的值。这个值是在上一轮计算的,所以没有问题。
但是,为什么在j=2时,dp_prev[i-k]是上一轮(j=1)的值?是的,因为我们在j=2开始前,dp_prev保存的是j=1的值。然后当j=2结束后,我们把dp_curr赋值给dp_prev,这样dp_prev变成j=2的值。然后j=3时,dp_prev就是j=2的值。
所以,正确。
但是,注意:对于同一个j,在遍历i时,我们更新max_val时,加入的是dp_prev[i-k],这个i-k是在递减吗?不,i是递增的,所以i-k也是递增的。所以加入的位置是递增的,因此我们只需要一个变量来维护最大值即可。
但是,我们加入的顺序是:当i=k+1时,加入位置1;i=k+2时,加入位置2;...;这样加入的值的位置是递增的,所以我们可以直接比较。
但是,如果最大值出现在中间某个位置,而后面出现一个更小的值,然后最大值还是保持不变。所以用max_val维护前缀最大值?不对,我们要求的是从1到i-k的所有位置的最大值,而随着i增加,i-k增加,所以区间在扩大,我们要维护的是当前区间内的最大值。而我们每次加入一个新的位置(i-k),然后更新最大值,那么当前的max_val就是区间[1, i-k]的最大值。所以正确。
因此,我们按照这个思路写代码。
但是,有一点需要注意:当i-k<1时,我们不更新max_val,所以max_val还是初始值(负无穷),那么当i<j时,dp_curr[i]保持负无穷;当i>=j时,如果max_val还是负无穷,那么dp_curr[i]也是负无穷(不可达)。只有当i>=j且i>=k+1(保证之前更新过max_val)并且max_val不为负无穷时,我们才更新。
另外,当k很大时(比如k>10000),那么i-k<1始终成立,那么max_val永远为负无穷,那么除了j=1,其他j>=2的状态都不可达。这是合理的,因为k太大,无法选出两个物品(因为第一个物品位置>=1,第二个物品位置必须>=1+k,而k>10000,n<=10000,所以第二个物品位置>10000,不存在)。所以最后答案也是负无穷?但是题目保证有解,所以k不会大于某个值?题目说任意两个物品位置差大于等于k,并且保证能取到m个物品,所以k不会太大(比如k<=n/(m-1)?不一定)。所以我们的代码会处理。
因此,我们输出代码。
但是,注意:当m=0时,我们之前没有处理。所以加上:
if (m==0) {
cout << 0 << endl;
return 0;
}
另外,当k=0?题目要求位置差大于等于k。如果k=0,那么位置差>=0,即可以任意选。但是题目要求位置差>=k,k最小是多少?题目没有说,但输入描述没有限制k的范围。但是示例输入k=2。所以k>=0?那么k=0时,位置差>=0,也就是可以重复选同一个位置?但题目说明是排成一行,所以同一个位置只能选一次。而且位置差定义为|i-j|?但是题目要求任意两个物品,所以位置差>=0是自动满足的(因为位置不同,位置差至少为1)?所以k=0时,实际上相当于没有位置差限制(因为任意两个物品位置差>=1>0)。所以k=0没有影响?但是题目要求任意两个物品的位置差都大于等于k。如果k=0,那么位置差>=0,而位置差的最小值(任何两个不同物品)为1>=0,所以满足。因此,k可以为0。但是,我们的代码中,当k=0时,i-k>=1这个条件在i>=1时恒成立(因为i>=1,所以i-k=i>=1),所以会更新max_val。但是,注意当k=0时,位置差>=0是自动满足的,所以我们可以选任意m个物品。
但是,当k=0,并且我们选两个物品时,要求位置差>=0,这是满足的。所以我们的代码会更新max_val,然后dp_curr[i] = max_val+a[i]。其中max_val是dp_prev[1]到dp_prev[i]的最大值(因为i-k=i,所以加入dp_prev[i])?不对,我们在循环中加入dp_prev[i-k]是在i-k>=1时,即i>=1(当k=0时,i>=1恒成立),所以加入的是dp_prev[i]。然后我们更新max_val。但是,当我们计算dp_curr[i]时,max_val是dp_prev[1]到dp_prev[i]的最大值?注意,我们的循环中,在位置i,我们先加入dp_prev[i](因为i-k=i),然后更新max_val,然后dp_curr[i] = max_val + a[i]。这样,dp_curr[i] = max{ dp_prev[1], dp_prev[2], ..., dp_prev[i] } + a[i]。这相当于求前缀最大值,然后加上当前a[i]。
这是正确的吗?当我们选j个物品,最后一个在位置i,那么前j-1个物品可以是位置1到i的任意位置(因为k=0,没有限制)。所以dp_curr[i] = max_{1<=t<=i} dp_prev[t] + a[i]。所以正确。
但是,注意:我们的状态要求最后一个物品是i,所以前j-1个物品的最后一个位置t必须小于i(因为物品位置递增)。但是,当k=0时,我们实际上允许t等于i?不,因为同一个位置不能选两次。所以t必须小于i。但是我们加入dp_prev[i]是在更新max_val之后,然后dp_curr[i] = max_val+a[i]中,max_val包含了dp_prev[i](即上一个物品在位置i,但是同一个位置不能选两次)。所以错误。
因此,我们需要避免同一个位置被选两次。所以,在加入dp_prev[i-k]时,实际上i-k和i是两个不同的位置吗?是的,因为位置差>=k=0,所以可以相邻(同一个位置不行,因为位置差为0,但题目要求位置差>=0?同一个位置的位置差为0,所以符合>=0?但是同一个位置只能有一个物品,所以不能重复选。因此,我们需要保证t不等于i。但是,在我们的状态中,t<=i-k,而k>=0,所以t<=i-0=i,但是t可以等于i(当k=0时)?所以我们需要避免t=i?因为同一个位置不能选两次。
但是,题目中物品排成一行,所以位置不同。所以每个位置只有一个物品。因此,我们选择物品时,每个位置只能选一次。因此,状态t和i一定不同(因为t<=i-k,当k=0时,t<=i,但t可以等于i?)。比如,当k=0时,我们允许t=i,那么我们就重复选择了位置i?这不允许。
所以,题目要求任意两个物品的位置差都大于等于k。如果k=0,那么同一个位置(位置差0)满足>=0,所以同一个位置可以选多个?但是题目中一个位置只有一个物品。所以实际上,我们只能选每个物品一次。所以位置差为0的情况只可能是同一个物品和自己,但是选择物品时,每个物品只能选一次。所以任意两个物品位置差>=1(因为位置不同)>0,所以k=0时,相当于没有限制(因为位置差至少1>=0)。所以实际上k=0和k=1是一样的?不对,当k=1,要求位置差>=1,而任意两个物品位置差至少1(因为不同位置),所以k=0和k=1都相当于没有额外限制?但是,当k=0时,题目允许位置差0,也就是允许同一个物品被选多次?但是题目没有说,而且物品只有一次。所以题目要求的是不同物品,位置差>=k。注意:题目要求“任意两个物品的位置差都大于等于k”,所以两个物品不能相同(位置相同),所以位置差至少1。所以k=0时,1>=0成立;k=1时,1>=1成立;k=2时,要求>=2,所以不能相邻。因此,k=0和k=1没有区别?但是,位置差至少1,所以k=0和k=1都满足。所以实际上k>=1和k=0没有区别?不对,k=0和k=1在位置差要求上是一样的(因为最小位置差1满足>=0和>=1)。所以k=0和k=1是一样的?那么为什么题目示例k=2?所以k=0和k=1都可以任意选?
因此,在动态规划中,当k=0时,我们实际上要求位置差>=0,而最小位置差1(因为位置不同)满足,所以相当于没有限制(除了不能重复选择同一个位置)。所以我们在状态转移时,上一个物品的位置t可以是从1到i-1(因为不能重复选同一个位置,所以t不能等于i),而不是1到i-k(当k=0时,1到i-k=1到i,这样包含了i)。所以错误。
因此,我们需要修改:在状态转移中,上一个物品的位置t必须小于i(因为位置不能重复)。所以,我们要求t<=i-k,同时我们必须要求t<i(因为位置不同)。所以当k=0时,t<=i,但是t必须小于i(因为不能重复)。所以我们应该要求t<=i-1?不对,如果k=0,那么位置差>=0,所以t可以是任意位置(除了i),所以t<=i-1(因为位置递增,所以t<=i-1)。
所以,我们修改:在状态转移中,上一个物品的位置t应该满足t<=i-k,并且t<=i-1(因为不能重复)。但是,当k>=1时,t<=i-k就已经保证了t<=i-1(因为k>=1,所以i-k<=i-1)。所以当k=0时,我们要求t<=i-1(因为不能重复)。因此,我们可以在动态规划中,当k=0时,令k=1?因为k=0和k=1在位置差要求上是一样的(都要求位置差>=1?不对,k=0要求位置差>=0,而k=1要求>=1,但是最小位置差1>=0和>=1都成立)。所以,我们可以将k=0视为k=1?因为实际约束相同(都不能重复选同一个位置,而且相邻位置差没有额外限制)。所以,在输入后,如果k==0,那么k=1?
或者,更严谨地,我们在状态转移时,要求上一个物品的位置t满足t<=i-1(避免重复),并且t<=i-k(题目要求位置差>=k,即i-t>=k)。所以t<=i-k且t<=i-1。由于i-k<=i-1(当k>=1时),所以取t<=i-k;当k=0时,i-k=i>i-1,所以我们就只能取t<=i-1。因此,我们可以将条件统一为:t<=min(i-1, i-k) ??? 不对,i-k可能大于i-1(当k<1),但k>=0,所以当k=0时,i-k=i>i-1,所以min(i-1,i-k)=i-1。
所以,我们修改:在状态转移时,我们要求t<=i-k且t<=i-1。那么实际上t<=max(i-k, i-1)吗?不对,应该是t<=min(i-1, i-k)?不对,应该是两个条件同时满足:t<=i-k且t<=i-1,所以t<=min(i-k, i-1)?不对,应该是t<=min(i-k, i-1)吗?不,应该是t<=i-k并且t<=i-1,所以t必须小于等于两者中的较小者?不对,比如k=0,我们需要t<=i-1(而不需要t<=i-k=i),所以就是t<=i-1。而k=1时,我们需要t<=i-1(因为重复)和t<=i-1(因为k=1,i-k=i-1),所以t<=i-1。因此,实际上,我们只需要t<=i-max(1,k)?不一定。
我们换一个思路:我们要求t满足两个条件:1. t<=i-1(避免重复) 2. t<=i-k(保证位置差>=k)。所以t的范围是[1, min(i-1, i-k)](当min(i-1,i-k)>=1时)。因此,在动态规划中,我们要求的前一个状态t的范围是[1, min(i-1, i-k)]。
因此,在优化中,我们维护的max_val应该是区间[1, min(i-1, i-k)]的最大值。那么,我们如何维护这个区间?
注意,该区间的右边界是min(i-1, i-k),即i-1和i-k的最小值。当k>=1时,i-k<=i-1,所以右边界是i-k;当k=0时,i-k=i,大于i-1,所以右边界是i-1。
所以,我们在加入t时,应该在t<=min(i-1, i-k)的范围内维护最大值。但是,在遍历i时,区间的右边界随着i的变化而变化。而且,当k=0时,右边界是i-1,那么我们在位置i时,需要加入dp_prev[i-1](而不是dp_prev[i-k],因为i-k=i,而我们不能加入dp_prev[i])。
因此,我们需要根据k来调整:
当k>=1时,我们加入dp_prev[i-k](当i-k>=1时)
当k=0时,我们加入dp_prev[i-1](当i-1>=1,即i>=2时)
所以,我们可以这样:
if (k==0) {
// 实际上k=0和k=1的约束效果一样,所以我们可以直接令k=1
k=1;
}
因为当k=0时,位置差>=0等价于k=1的位置差>=1(因为最小位置差为1,满足>=0和>=1)。
这样,我们就不需要特殊处理k=0,直接令k=1即可。而且题目保证有解,所以k=0时,我们按k=1处理,不会影响答案。
所以,我们在读入k后,如果k==0,则将k设为1。
此外,还有k<0?题目没有说k的范围,但位置差不能为负,所以k非负。题目描述中说“位置差都大于等于k”,所以k>=0。
因此,我们在代码开始加上:
if (k==0) k=1;
这样,k至少为1。那么,在状态转移时,我们加入的位置是i-k,且i-k<=i-1(因为k>=1),所以不会重复。
所以,修改后的完整代码:
if (k==0) k=1;
// 然后按照上面的动态规划进行
这样,当k=0时,我们按k=1处理。
但是,注意:题目要求位置差>=k,当k=0时,位置差>=0,而按k=1处理变成了位置差>=1,是否等价?在最小位置差为1的情况下,>=0和>=1都满足,所以是等价的。所以正确。
因此,最终代码:
注意:使用long long,以及初始化负无穷为-1e18(足够小)。
另外,当m=0时,输出0。
但是,题目中m<=100,m>=1?但输入可能m=0?所以加上判断。
最后,我们输出代码。
但是,注意:当k很大时,比如k>n,那么当j>=2时,所有的dp_curr[i]都是负无穷(因为i-k<1,所以max_val不更新,并且i>=j>=2,所以dp_curr[i]为负无穷)。那么最后我们取最大值,dp_prev数组(j=m)全部负无穷,然后输出一个负无穷?题目保证有解,所以不会出现这种情况。所以没关系。
因此,我们编写代码。
但是,上面的动态规划中,我们只更新了最后一个物品在位置i的状态,所以最后答案是所有i中dp_prev[i]的最大值。
但是,题目要求的是在n个物品中选m个,不一定最后一个物品是哪个。所以正确。
现在,我们按照这个思路写代码。
但是,有一个小问题:当我们更新dp_curr[i]时,我们要求i>=j(表示位置i是第j个物品,那么前面至少要j-1个位置,所以i>=j)。这是合理的。
然而,在示例输入:4 2 2,数组[2,4,-6,1](位置1到4:a[1]=2, a[2]=4, a[3]=-6, a[4]=1)
按我们的动态规划:
初始化dp_prev: [ -1e18, 2, 4, -6, 1] (下标0不用,下标1~4)
j=2:
i=1: 1<2,所以dp_curr[1]不更新(保持-1e18)
i=2: i>=2,但是i-k=0<1,所以不更新max_val(max_val=-1e18),所以dp_curr[2]=-1e18(不可达)
i=3: i-k=1>=1,所以加入dp_prev[1]=2,更新max_val=2。然后i=3>=2,所以dp_curr[3]=2+a[3]=2-6=-4。
i=4: i-k=2>=1,所以加入dp_prev[2]=4,更新max_val=max(2,4)=4。然后dp_curr[4]=4+a[4]=4+1=5。
所以dp_curr: [ -1e18, -1e18, -1e18, -4, 5]
然后dp_prev更新为dp_curr(即j=2的状态)
然后,最后在dp_prev中找最大值,为5。输出5。正确。
所以,我们输出优化后的代码。
注意:空间优化,我们只用了两个一维数组。
代码实现(C++):
</think>为了解决牛牛在背包限制下选择受欢迎程度之和最大的物品集合,且任意两个物品位置差至少为k的问题,我们需要优化原有的时间复杂度为O(n^2*m)的动态规划方法。原方法使用三重循环,在n和m较大时效率较低。优化思路是利用动态规划的递推关系,将内层循环简化为O(1)操作,从而将总时间复杂度降为O(n*m)。
### 优化思路
1. **动态规划状态定义**:
- `dp[j][i]` 表示考虑前 `i` 个物品,选择 `j` 个物品(且最后一个物品位置为 `i`)时的最大欢迎度之和。
2. **状态转移优化**:
- 对于状态 `dp[j][i]`,需要从 `i-k` 之前的物品中选择 `j-1` 个物品的最大值加上当前位置 `i` 的欢迎度。
- 使用一个变量 `max_val` 在遍历过程中维护区间 `[1, i-k]` 内 `dp[j-1][t]` 的最大值,避免每次重复计算。
3. **边界处理**:
- 当 `k = 0` 时,等价于 `k = 1`(因为最小位置差恒为1)。
- 初始化 `j=1` 的状态为每个物品自身的欢迎度。
- 对于 `j > 1`,若 `i < j` 或无法满足位置差约束,则状态不可达(保持负无穷)。
4. **空间优化**:
- 使用两个一维数组 `dp_prev` 和 `dp_curr` 分别存储 `j-1` 和 `j` 的状态,避免二维数组开销。
### 优化代码
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
int n, m, k;
cin >> n >> m >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
if (m == 0) {
cout << 0 << endl;
return 0;
}
if (k == 0) k = 1;
// dp_prev[i]: 最后一个物品为i且已选j个时的最大和(初始化j=1)
vector<long long> dp_prev(n + 1, -1e18);
for (int i = 1; i <= n; i++) {
dp_prev[i] = a[i];
}
if (m == 1) {
long long ans = *max_element(dp_prev.begin() + 1, dp_prev.end());
cout << ans << endl;
return 0;
}
vector<long long> dp_curr(n + 1, -1e18);
for (int j = 2; j <= m; j++) {
long long max_val = -1e18;
for (int i = 1; i <= n; i++) {
if (i - k >= 1) {
if (dp_prev[i - k] > max_val) {
max_val = dp_prev[i - k];
}
}
if (i >= j) {
if (max_val != -1e18) {
dp_curr[i] = max_val + a[i];
}
}
}
dp_prev = dp_curr;
fill(dp_curr.begin(), dp_curr.end(), -1e18);
}
long long ans = -1e18;
for (int i = 1; i <= n; i++) {
if (dp_prev[i] > ans) ans = dp_prev[i];
}
cout << ans << endl;
return 0;
}
```
### 复杂度分析
- **时间复杂度**:O(n × m),其中 `n` 为物品数量,`m` 为需选择的物品数。双重循环分别遍历 `j`(2 到 `m`)和 `i`(1 到 `n`)。
- **空间复杂度**:O(n),使用两个一维数组存储状态。
### 注意事项
1. **负数处理**:使用足够小的负无穷(`-1e18`)表示不可达状态。
2. **大数溢出**:使用 `long long` 存储中间结果,避免累加溢出 `int` 范围(题目保证最终答案在 `int` 内)。
3. **边界条件**:处理 `m=0` 和 `m=1` 的情况,以及 `k=0` 时等价于 `k=1`。
此优化方法有效减少了时间复杂度,适用于题目约束(`n ≤ 10000`, `m ≤ 100`)。