题解 | 加减
加减
https://ac.nowcoder.com/acm/contest/11214/I
回溯
看到这题的第一反应是回溯,规定dfs(i, j)表示考虑前i个数字,还剩下j次修改;对于每一个数字,都有2种选择:
- 选择它:j-1
- 可以+1也可以-1
- 选了第i个数,下一次还能接着选(可以剪枝:例如上一次是加1,那么再选它也应该是加1,否则没有意义)
- 不选择它:j不变
这是最朴素的回溯,而且没有剪枝,肯定是超时了(后来我用对数器和下面的代码验证了一下(n=30,k=5,100组),基本可以认为它是正确的。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+10;
int n, k, a[N];
int res = 0;
void dfs(int i, int j)
{
if (i < 0 || j == 0)
{
unordered_map<int, int> map;
for (int i = 0; i < n; i++) map[a[i]]++;
for (auto &e : map) res = max(res, e.second);
return;
}
// 选,-1
a[i]--;
dfs(i - 1, j - 1);dfs(i, j - 1);
a[i]++;
// 选,+1
a[i]++;
dfs(i - 1, j - 1);dfs(i, j - 1);
a[i]--;
// 不选
dfs(i - 1, j);
}
signed main()
{
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
dfs(n - 1, k);
cout << res;
}
前缀和+滑动窗口
这道题要求的是经过k次加减操作后的序列中最大的相同元素个数,那么排序过后,这些元素就会挨在一起。那么问题转化为对一个排序后的数组,求一个区间在k次加减操作后,使得区间内的元素都相同,且区间长度最长。
算法核心:对于一组排序后的数,并想通过最少的增减操作将它们全部变成同一个数时,选择中位数作为目标值可以最小化所需的总操作次数。这是因为中位数最靠近所有元素的中间位置,调整这些数字到中位数的代价最小。
函数 f(l, r)
计算将区间 [l, r]
内所有元素变为 a[mid]
所需的操作次数。核心思想是:将左边的元素都增加到 a[mid]
,将右边的元素减少到 a[mid]
,然后计算总的操作次数。
公式的推导过程如下:
s[r] - s[mid]
:[mid+1, r]
区间内所有元素的和。s[mid-1] - s[l-1]
:[l, mid-1]
区间内所有元素的和。a[mid] * (r - mid)
:[mid+1, r]
区间内所有元素都变为a[mid]
所需的操作次数。a[mid] * (mid - l)
:[l, mid-1]
区间内所有元素都变为a[mid]
所需的操作次数。
前缀和:计算数组 a
的前缀和数组 s
,s[i]
表示前 i
个元素的总和。
滑动窗口的思路还是比较直观的:找到一个窗口,使得它尽量通过小于或等于k次的操作得到,这里用了一点小贪心,因为如果对一个数字进行多次操作,那么操作肯定都是一样的(只加或者只减,加减混用相当于都浪费了),所以用的总操作数越多,最终得到的区间也就越长。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+10;
int a[N], s[N];
int f(int l, int r)
{
int mid = (l + r) >> 1;
int sub_sum = s[r] - s[mid] - (s[mid - 1] - s[l - 1]);
int op_sum = a[mid] * (r - mid) - a[mid] * (mid - l);
return sub_sum - op_sum;
}
signed main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
int left = 1, right = 1, res = 0;
while (right <= n)
{
while (f(left, right) > k) left++;
res = max(res, right - left + 1);
right++;
}
cout << res << endl;
}
二分优化
二分查找:
- 外层循环遍历数组中的每个元素,以它为区间的左边界
i
。 - 使用二分查找来确定右边界
mid
,以便在k
次操作内尽可能扩展区间[i, mid]
。 - 每次通过
f(i, mid)
计算操作次数,如果次数不超过k
,则更新答案res
为当前区间长度mid - i + 1
。 - 二分查找结束后,输出
res
,即为最大化某个元素频率后的最大出现次数。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+10;
int a[N], s[N];
int f(int l, int r)
{
int mid = (l + r) >> 1;
int sub_sum = s[r] - s[mid] - (s[mid - 1] - s[l - 1]);
int op_sum = a[mid] * (r - mid) - a[mid] * (mid - l);
return sub_sum - op_sum;
}
signed main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
int res = 0;
for (int i = 1; i <= n; i++)
{
int l = i, r = n; // 遍历每一个区间
while (l <= r)
{
int mid = (l + r) >> 1;
if (f(i, mid) <= k)
{
l = mid + 1;
res = max(res, mid - i + 1);
}
else r = mid - 1;
}
}
cout << res << endl;
}