给定一个长为 \(n\) 的数组 \(a\) ,你可以对任意 $ i, j ;(i\neq j)$ ,进行操作 \(a_j=a_i+a_j\)。同时,需要满足数组中任意一项值不超过 \(k\),求最多可进行多少次操作。
*800
greedy
这题,显然是一个离线贪心。不了解的可以查看我的贪心算法专题、
排序之后,把最小值不断赋值给其他数直到无法继续赋值。
对于这种题,毫无疑问贪心算法或者数学法会是更好的选择。
const int maxn = 1e5 + 50;
int f[maxn], mn, pos;
void solve(){
int n = read(), k = read();
for (int i = 0; i < n; ++ i) f[i] = read();
sort(f, f + n);
LL sum(0);
for (int i = 1; i < n; ++ i) sum += (k - f[i]) / f[0];
cout << sum << ‘\n‘;
}
给定一个长为 \(n\) 的非负整数序列 \(a\) ,和一个定值 \(T\)。
定义 \(f(b)\) 为:
? 序列 \(b\) 中,满足 \(b_i + b_j = T\) 的点对 \((i, j) \; i < j\) 的总数。
现在,要求对序列 \(a\) 进行黑白染色,使其分为两个子序列 \(c, d\) 。求问使得 \(f(c) + f(d)\) 最小的染色方案。
*1100
本题具有两个思路,分别为 基于数学推演的情况讨论法,基于贪心算法的暴力比较法。
假如在竞赛过程中,如果能大致保证贪心算法的正确性时,用贪心算法显然更好一些。
这里主要介绍贪心算法,数学推演实际上是将小于 \(\dfrac{T}{2}\) 的分为一组, 大于 \(\dfrac{T}{2}\) 的分为一组。等于 \(\dfrac{T}{2}\) 的在两子序列中周期摆动安放。
而贪心算法则是维护两个子序列的 hashmap
。当要插入一个数字时,比较其在两个子序列中分别能组成多少组合法点对,贪心选择数量较少的进行插入,满足要求。
在看到题面的时候,通过注意到 \(b_i + b_j = T\) ,我们应该快速联想到 hashmap
解决此类问题。
在看到最小时,应该快速思考使用在线 or 离线贪心算法。
const int maxn = 1e5 + 50;
int f[maxn], ans[maxn];
void solve(){
int n = read(), T = read();
for (int i = 0; i < n; ++ i) f[i] = read();
unordered_map<int, int> mp1, mp2;
mp1.clear(), mp2.clear();
VI ans(n);
for (int i = 0; i < n; ++ i){
if (mp1[T - f[i]] <= mp2[T - f[i]]){
ans[i] = 0;
++ mp1[f[i]];
}else {
ans[i] = 1;
++ mp2[f[i]];
}
}
show(ans);
}
给定一个长为 \(n\) 的序列 \(a\),该序列元素大小均在 \([1, n]\) 中。定义 k惊喜数字:
- 在序列 \(a\) 所有长为 \(k\) 的连续子序列中均出现过的最小元素,若不存在则赋值为 \(-1\)。
请你计算并输出 \(k \in [1, n]\) 的所有 k惊喜数字。
1 <= n <= 3e5
*1500
刚拿到这题时,我没有一点思路。一直在思考是不是滑动窗口,以及如何复用其中的信息,均发现无法快速解决。
后面转换角度从每个元素的特性出发,插入头尾两个虚拟结点,寻找各元素之间的最大间隔。并发现这个方法可行。
最终得出如下结论:
- 假如你不自觉的想入一个问题且在一定时间内不能得出答案时,说明你有一个细节或者角度不对。
- 如果不自觉的用宏观解决问题时(比如在段,区间),专注于微观可能是这个题目的解决方案(位,各元素之间)
之所以写上面这段话,是因为这题当时险些没有做出,而且不属于方法不清楚的层面,而是我在思考问题时出现了角度的偏差。谨以此警示。
回归这个题目,如上文所提,我们维护每个元素的最大间隔。易知,当 \(k\) 大于某元素最大间隔时,就可能对k惊喜数字产生贡献。在利用 std::map
维护每元组的间隔时,由于其自动排序的特性,可以从头至尾进行处理。
const int maxn = 3e5 + 50;
int f[maxn];
void solve(){
int n = read(), tot = 0, cnt(0);
map<int, int> dis; // 最大间距
unordered_map<int, int> pre; // 上一个元素出现位置
for (int i = 1; i <= n; ++ i){ // index from 1 (in order to insert 0-index node)
f[i] = read();
if (pre[f[i]] == 0){
pre[f[i]] = i;
dis[f[i]] = i;
}else {
int dlt = i - pre[f[i]]; // 间距
// wprint(dlt, dis[f[i]]);
pre[f[i]] = i;
if (dlt > dis[f[i]]) dis[f[i]] = dlt;
}
}
for (auto &e: pre){
int dlt = n + 1 - e.second; // insert (n+1)-index node
if (dlt > dis[e.first]) dis[e.first] = dlt;
// wprint(dlt, e.first, e.second);
}
vector<int> ans(n + 1, -1);
int stop = n + 1;
for (auto &e: dis){ // 利用 map 的排序特性,从头至尾处理即可
if (e.second >= stop) continue;
for (int i = e.second; i < stop; ++ i) ans[i] = e.first;
stop = e.second;
}
for (int i = 1; i <= n; ++ i) cout << ans[i] << (i == n ? ‘\n‘ : ‘ ‘);
}
- 出一个序列 \(a\),求出一个长度不超过 \(3n\) 的操作序列,每次操作之后序列中所有元素必须为非负整数,操作完成后使序列 \(a\) 中每个元素相等。
- 定义一次操作为:选出 \((i,j,x)\) 三元组,满足 \(i,j\) 为序列合法下标,\(x\) 为 \(10^9\) 以内非负整数,令 \(a_i:= a_i-x\cdot i,a_j:=a_j+x\cdot i\)。
- 输出时先输出操作次数 \(k\),然后输出 \(k\) 行操作序列。
*2000
*2000
分的构造题,实际上比完赛之后想想还是很套路的。以后有机会总结一个 你只能用最多 \(c \cdot n\) 次操作,完成一个目标。
在这里进行一个简单的总结。
用 c * n 次操作(询问)完成目标 首先,不管可用操作(询问)数为多少,一个永远可以尝试的思路是: 以某个最特殊的元组做跳板。
- 若 \(c == 1\),则为线性操作类似冒泡的感觉,从头至尾线性操作,每次保留其中之一在下一次进行操作。
- 若 \(c == 2\),一般来说:
- 可能为 \(c == 1\)时的情况,但是每次操作需要两步C. Chocolate Bunny 类似这题。
- 或者对每个元素用一次操作将其转移到跳板上,再进行一次操作完成目的。
- 若 \(c==3\),则想法与上述差不多。
总而言之,需要发现操作的特定性质,如互相操作,跳板操作。
对于这题,比较困难的地方在于每次操作之后保证所有的元素非负。根据上述结论显然我们需要选择一共“跳板
”,毫无疑问这个跳板选择初始位置 \(1\) 最为合适。因此,我们大致制定好了构造策略:
根据题设限制,每个元素最多转移 \(a_i - (a_i) \%i\) ,对于余下的 \(a_i \% i\) ,我们可以先补充上 \(i - (a_i \% i)\),再进行转移。经过思考,这个方案是满足条件的,实际上他也符合一种贪心的思想。我们尽可能避免对于元素进行较大的减操作。
因此最终策略为:
const int maxn = 1e4 + 50;
int f[maxn];
void solve(){
int tot(0), n = read();
for (int i = 1; i <= n; ++ i) f[i] = read(), tot += f[i];
if (tot % n != 0){ cout << "-1\n"; return; } // 不能整除显然不行
int eve = tot / n;
vector< tuple<int, int, int> > res;
for (int i = 2; i <= n; ++ i){
if (f[i] % i == 0) res.pb({i, 1, f[i] / i});
else {
res.pb({1, i, i - (f[i] % i)});
res.pb({i, 1, (f[i] / i) + 1});
}
}
for (int i = 2; i <= n; ++ i) res.pb({1, i, eve});
wprint(sz(res));
for (auto &e: res) wprint(get<0>(e), get<1>(e), get<2>(e));
}
给定长度为 \(n\) \((1\le n\le3\times 10^5)\) 的数列 \(\{a_n\}(0\le a_n\le 10^9)\),请求出最小的整数 \(x\) 使 \(\{a_n\oplus x\}\) 的逆序对数最少,其中$ \oplus$ 是异或。
*2000
divide and conquer
CDQ分治
本题最开始会比较容易想到通过 dp
去解决。考虑到逆序对这个经典的问题,便考虑到分治的思路。实际上 bit
的 0 or 1 可以类比为二叉树的左右孩子。若越靠近根结点代表的 bit
越高,则右孩子一定大于左孩子(用了字典树的知识)。
因此,题目便比较简单了:运用 CDQ 分治的思路:
首先,我们维护一个 dp
数组,dp[bit][i]:= x 的第 bit 位为 i 时逆序对的个数
。
因为,两个元素比较大小等价于比较两者第一个不同位。不同位为 1
的更大。因此在解题的过程中,维护两个数组,左边代表当前分治到左边的元素(bit is 0
),右边代表当前分治到右边的元素(bit is 1
) 。因此,右边一定大于左边,在利用求解逆序对的知识,通过双指针处理两个子树合并出现的逆序对个数。
bit
(等价于 x
的第 bit
位为 0
),则逆序对为直接求得的。bit
(等价于 x
的第 bit
位为 1
),则逆序对数为总数-不修改时的逆序对个数。之后,贪心选取其中每 bit
逆序对较小的构造答案。
#define pb push_back
using VI = vector<int>;
using LL = long long;
// CDQ 分治处理点对问题
int dp[35][2];
void helper(VI cur, int bit = 30){
if (bit < 0 or cur.empty()) return;
int cnt1(0), ans1(0);
int cnt2(0), ans2(0);
VI right, left;
for (auto &x: cur){
if ((x >> bit) & 1){
ans1 += cnt2; // 由于 index 从大到小,因此 cnt2 代表 index 大于当前元素且元素值小于当前元素的点对数。
++ cnt1;
right.pb(x);
}else {
ans2 += cnt1;
++ cnt2;
left.pb(x);
}
}
helper(left, bit - 1), helper(right, bit - 1);
dp[bit][0] += ans1;
dp[bit][1] += ans2;
}
void solve(){
int n = read();
VI a(n);
for (auto &&e: a) e = read();
reverse(all(a));
helper(a);
LL ans(0), res(0);
for (int i = 0; i <= 30; ++ i){
ans += min(dp[i][0], dp[i][1]);
if (dp[i][1] < dp[i][0]) res |= (1 << i);
}
cout << ans << ‘ ‘ << res << ‘\n‘;
}
Codeforces Round #673 (Div. 2)[A-E]
原文:https://www.cnblogs.com/Last--Whisper/p/13757291.html