不知不觉已经过了差不多一个月了,坚持一件事情还是有点收获的,今天更新一道1600的构造。
寒假训练计划day17
摘要:
Part1 题意
Part2 题解 (有数学推导,latex形式)
Part3 代码 (C++版本,有详细注释)
Part4 我对构造题的归纳总结(自己对构造题的浅薄理解还有例题)
题目链接(有需自取):Problem - 1781C - Codeforces
Part1 题意
题意:
规定字符串是平衡字符串当且仅当字符串中每个字符的数量相等,给定一个长度为的字符串S,要求找到一个长度为的平衡字符串T,使得满足T和S的字符匹配数最多,也就是最大,输出不匹配的数量和构造出来的T。
Part2 题解
题解:
1、已知, 设为字符串的匹配数量并赋初值。
2、我们来观察一下平衡字符串有什么性质,因为在平衡字符串中,每个字符的数量都是相等的,假设我们知道每个字符的数量都是,显然, 这两个数本质就是的约数。所以我们对分解约数,找到所有满足的数对。
3、假设我们当前考虑,我们需要求出当前这样分配最多能匹配几个字符,因为每个字符在原字符串中的数量不同,所以我们预处理一个序列,他们存的是S中的字符和字符的数量,并且排序,再处理出来一个该序列的前缀和序列,根据这两个序列我们可以开始求最多能匹配几个字符,我们先二分出大于等于的位置,再进行分了讨论,具体如下:
// u1是cnt, u2是hve, v是匹配数量 int u1 = c, u2 = n / c; int l = 1, r = cnt, v; if(u1 <= 26) { // 先二分 while(l < r) { int mid = l + r >> 1; if(q[mid].ff >= u2) r = mid; else l = mid + 1; } // 分类讨论 v = -1e18; if(q[l].ff >= u2) { if(cnt - l + 1 >= u1) v = u1 * u2; else v = (cnt - l + 1) * u2 + qs[l - 1] - qs[max(0ll,l - 1 - (u1 - (cnt - l + 1)))]; } else { v = qs[cnt] - qs[max(0ll,cnt - u1)]; } // 更新res if(res < v) { res = v; usu1 = u1, usu2 = u2; } }
最后得到了使得匹配数量最大的。
4、现在我们需要利用逆向构造出T,首先我们对于已经处理出来的序列(已排序)的后面个,注意可以会有不足的情况,我们就得补充回去,具体操作如下:
cout << n - res << endl; // 输出最后不匹配的最小数量 vector<char> us; // 先处理在原字符串中的字符 for(int i = cnt; i >= max(1ll, cnt - usu1 + 1); i--) { us.push_back(q[i].ss); st[q[i].ss - 'a'] = usu2; } // 先将原字符串的字符有st标记的填入 for(int i = 0; i < n; i++) if(st[s[i] - 'a']) { ans[i] = s[i]; st[s[i] - 'a']--; is_use[i] = 1; } // 将后面的补上 for(int i = max(1ll, cnt - usu1 + 1) - 1; i >= 1; i--) us.push_back(q[i].ss), st[q[i].ss - 'a'] = usu2; // 如果不够再补上一些没用过的字符 for(int i = 0; i <= 25; i++) if(!ot[i]) us.push_back(i + 'a'), st[i] = usu2; int l = 0; // 最后从入序列的顺序补全字符串 for(int i = 0; i < n; i++) { while(!st[us[l] - 'a']) l++; if(!is_use[i]) ans[i] = us[l], st[us[l] - 'a']--; } for(int i = 0; i < n; i++) cout << ans[i]; cout << endl;
Part3 代码(C++):
#include <bits/stdc++.h> #define int long long #define ff first #define ss second using namespace std; using PII = pair<int, char>; constexpr int N = 1e6 + 10; int n, m, sum; int ot[50], qs[50], is_use[N], st[50], cnt; PII q[50]; void solve() { string s; cin >> n >> s; cnt = 0; for(int i = 0; i <= 30; i ++ ) ot[i] = 0, st[i] = 0; for(int i = 0; i < n; i ++ ) ot[s[i] - 'a'] ++, is_use[i] = 0; for(int i = 0; i <= 30; i ++ ) if(ot[i]) q[++ cnt] = {ot[i], 'a' + i}; sort(q + 1, q + 1 + cnt); for(int i = 1; i <= cnt; i ++ ) qs[i] = q[i].ff + qs[i - 1]; int res = -1e18; vector<char> ans(n + 1, 0); int usu1,usu2; for(int c = 1; c <= n / c; c ++ ) if(n % c == 0) { int u1 = c, u2 = n / c; int l = 1, r = cnt, v; if(u1 <= 26) { while(l < r) { int mid = l + r >> 1; if(q[mid].ff >= u2) r = mid; else l = mid + 1; } v = -1e18; if(q[l].ff >= u2) { if(cnt - l + 1 >= u1) v = u1 * u2; else v = (cnt - l + 1) * u2 + qs[l - 1] - qs[max(0ll,l - 1 - (u1 - (cnt - l + 1)))]; } else { v = qs[cnt] - qs[max(0ll,cnt - u1)]; } if(res < v) { res = v; usu1 = u1, usu2 = u2; } } u1 = n / c, u2 = c; if(u1 <= 26) { l = 1, r = cnt; while(l < r) { int mid = l + r >> 1; if(q[mid].ff >= u2) r = mid; else l = mid + 1; } v = -1e18; if(q[l].ff >= u2) { if(cnt - l + 1 >= u1) v = u1 * u2; else v = (cnt - l + 1) * u2 + qs[l - 1] - qs[max(0ll,l - 1 - (u1 - (cnt - l + 1)))]; } else { v = qs[cnt] - qs[max(0ll,cnt - u1)]; } if(res < v) { res = v; usu1 = u1, usu2 = u2; } } } cout << n - res << endl; vector<char> us; for(int i = cnt; i >= max(1ll, cnt - usu1 + 1); i -- ) { us.push_back(q[i].ss), st[q[i].ss - 'a'] = usu2; // cout << q[i].ss << endl; } for(int i = 0; i < n; i ++ ) if(st[s[i] - 'a']) { // cout << s[i] << endl; ans[i] = s[i], st[s[i] - 'a'] --, is_use[i] = 1; // cout << ans[i] << "#"; } for(int i = max(1ll, cnt - usu1 + 1) - 1; i >= 1; i -- ) us.push_back(q[i].ss), st[q[i].ss - 'a'] = usu2; for(int i = 0; i <= 25; i ++ ) if(!ot[i]) us.push_back(i + 'a'), st[i] = usu2; int l = 0; for(int i = 0; i < n; i ++ ) { while(!st[us[l] - 'a']) l ++; if(!is_use[i]) ans[i] = us[l], st[us[l] - 'a'] --; } for(int i = 0; i < n; i ++ ) cout << ans[i]; cout << endl; } signed main() { int ts; cin >> ts; while(ts --) solve(); return 0; }
Part4 我对构造题的归纳总结(自己对构造题的浅薄理解)
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