今天更新一道1700的构造。
Part1 题意, 链接(有需自取,Problem - 1779D - Codeforces)
Part2 题解
Part3 代码(C++代码)
Part4 每日回顾一个基础算法|数据结构计划(今日:树状数组)
Part5 对构造题尝试总结(附带例题)
题意:
给定长度为的数组, 再给定一个长度为的数组,其中,每次可以对数组进行以下操作,使用数组的一个元素(注意的每个元素只能使用一次),再选择任意的,使得,问能否使得变成,能输出"YES",否则输出"NO"。
题解:
1、引出问题:我们考虑如何构造一个方案使得a经过这个方案操作后不存在冲突就是有解的,否则判断一定无解。2、先根据的值进行分块,因为最大达到了所以先对哈希,映射到存在中,记录每个的数量为。
3、考虑一下从小到大贪心,每次先处理小的,我们可以发现这种情况下我们必须绕过比较大的,因为一旦将较大值改成较小值,往后无法再将其改回来。
4、此时我们不妨逆着思考,从大到小这样就可以边维护边去判断,每次我们就从大到小遍历值相同的块,例如当,序列为 , 因为我们每次都是对区间操作,如果中间存在较大块我们应该绕过它,使得需要的刀数+1。
4.1 怎么维护已经被处理过的较大块, 我们可以用树状数组进行标记,当处理完当前块时,我们让每个位置+1, 当我们需要知道某个区间是否存在已经处理的块需要跳过,我们直接的查询前缀和是否为0即可,遍历每个值进行标记最多也就是。具体代码如下:
for(int i = 0; i < us[t].size(); i ++ ) add(us[t][i], 1);
4.2 对于怎么得到最小需要刀数, 对于每个块我们最多是的去查询,每次我们可以从起点出发判断与下一个索引是否之间的树状数组标记是否是大于0的,大于0要继续分,为0可以合并下一个,还要特判一种情况,当满足时,我们应该直接跳过,但是能合并可以直接合并,因为它等于我们的块,但是他不应该再多拿块,具体代码如下:
int c = 0; for (int i = 0; i < us[t].size(); i++) { if (a[us[t][i]] == b[us[t][i]]) continue; int j = i; ++c; while (j + 1 < us[t].size() && sum(us[t][j + 1]) - sum(us[t][j]) == 0) ++j; i = j; }
4.3 该贪心策略的合理性,因为大值块不影响小值的块,且考虑了比当前较大的块的位置,所以一定没有冲突。
5、根据这个流程往下,边做边判断即可得到答案。
#include
#define int long long #define lowbit(x) (x&-x) using namespace std; constexpr int N = 1e6 + 10; int n, m, idx; int a[N], b[N], rz[N]; int tr[N]; // 树状数组标记数组 // mp是将b[i]映射到0-2e5数字上面 // _mp是mp的反映射,"反函数" // rz[i]是剃刀的长度数组 map mp, _mp, cnt; void add(int x, int c) { for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; } int sum(int x) { int res = 0; for(int i = x; i; i -= lowbit(i)) res += tr[i]; return res; } // 按照b的大小进行排序,这样子可以从大到小直接遍历 bool cmp(int x, int y) { return _mp[x] > _mp[y]; } void solve() { mp.clear(); _mp.clear(); cnt.clear(); bool flag = 1; idx = 0; cin >> n; for(int i = 0; i <= n ; i ++ ) tr[i] = 0; for(int i = 1; i <= n; i ++ ) cin >> a[i]; for(int i = 1; i <= n; i ++ ) { cin >> b[i]; if(a[i] < b[i]) flag = 0; } cin >> m; for(int i = 1; i <= m; i ++ ) { cin >> rz[i]; cnt[rz[i]] ++; } vector B; for(int i = 1; i <= n; i ++ ) { if(!mp[b[i]]) { mp[b[i]] = ++ idx; _mp[idx] = b[i]; } if(a[i] != b[i] && !cnt[b[i]]) { flag = 0; break; } } if(!flag) { cout << "NO" << endl; return; } for(int i = 1; i <= idx; i ++ ) B.push_back(i); sort(B.begin(), B.end(), cmp); vector us[idx + 2]; for(int i = 1; i <= n; i ++ ) us[mp[b[i]]].push_back(i); for(auto t : B) { int c = 0; for(int i = 0; i < us[t].size(); i ++ ) { if(a[us[t][i]] == b[us[t][i]]) continue; int j = i; ++ c; while(j + 1 < us[t].size() && sum(us[t][j + 1]) - sum(us[t][j]) == 0) ++ j; i = j; } if(cnt[_mp[t]] < c) { flag = 0; break; } for(int i = 0; i < us[t].size(); i ++ ) add(us[t][i], 1); } cout << (flag ? "YES" : "NO") << endl; } signed main() { int ts; cin >> ts; while(ts --) solve(); return 0; }
默写回顾了一下板子。
1、树状数组的单点查询和单点修改模板:int tr[N]; int lowbit(int x) { return (x & -x); } // 在x位置上面增加c void add(int x, int c) { for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; } // 求到x的前缀和 int sum(int x) { int res = 0; for(int i = x; i; i -= lowbit(i)) res += tr[i]; return res; }
2、树状数组的区间修改和区间查询:
int tr1[N], tr2[N]; int lowbit(int x) { return (x & -x); } void add(int tr[], int x, int c) { for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; } int sum(int tr[], int x) { int res = 0; for(int i = x; i; i -= lowbit(i)) res += tr[i]; return res; } int get(int l) { return (l + 1) * sum(tr1, l) - sum(tr2, l); } signed main() { scanf("%lld%lld", &n, &m); for(int i = 1; i <= n; i ++ ) scanf("%lld", &a[i]); for(int i = 1; i <= n; i ++ ) add(tr1, i, a[i] - a[i - 1]), add(tr2, i, i * (a[i] - a[i - 1])); while(m --) { char op[2]; int l, r, d; scanf("%s%lld%lld", op, &l, &r); if(*op == 'C') // 区间修改 { scanf("%lld", &d); add(tr1, l, d), add(tr1, r + 1, -d); add(tr2, l, l * d), add(tr2, r + 1, -(r + 1) * d); } else { // 区间查询 printf("%lld\n", get(r) - get(l - 1)); } } return 0; }
1、前后缀贪心,比如说观察前后缀的sum,去看以后怎么考虑最好。Problem - 1903C - Codeforces
2、双指针贪心法,考虑两端相消或者相互作用,还有就是考虑左右边界。 Problem - 1891C - Codeforces
Problem - 1907D - Codeforces
3、转换观察法,有些关系可以抽象成图,观察图的某些性质去总结规律。也可以抽象成一个集合,两个集合相等可以说明有解可构造。Problem - 1891C - Codeforces
4、打表找规律,一般没什么规律可循即可打表找规律,一般和数论有关的很喜欢考,acm也喜欢考,属于人类智慧题。Problem - 1916D - Codeforces
5、公式推导演算,常见的分为公式的等价变形、公式的化简(这个常考,一般需要先证明某些性质,可以直接抵消,一般如果原公式处理起来很复杂时就可以考虑)。Problem - 1889B - Codeforces
6、考虑奇偶数去简化问题或者分类问题,从其中的一些运算性质入手,因为奇数偶数的加减以及%运算(这个结论很重要)的结果的奇偶性是固定的,Problem - 1898C - Codeforces
7、根据性质构造模型,看看能不能分成几个块,几个不同的集合,再选择算法去解决。Problem - 1873G - Codeforces
8、考虑从小到大处理,或者是从大到小处理,有时候先处理小的对大的不会有影响,或者反过来,这样的处理顺序是最完美的。Problem - 1904D2 - Codeforces
9、边界贪心法,一般要在问题的最边界处考虑,有时候这样做结果是最优的,或者考虑边界上的影响,假如让影响最小,就使得影响<= 固定值 。 Problem - E - Codeforces and Problem - 1903C - Codeforces