2019/11/12 CSP模拟赛&&考前小总结
写在前面的总结
离联赛只有几天了,也马上就要回归文化课了。
有点舍不得,感觉自己的水平刚刚有点起色,却又要被抓回文化课教室了,真想在机房再赖几天啊。
像19/11/11那场的简单题,自己还是能敲出一些比较稳的暴力,虽然不见得能拿很高档的暴力或者打出正解,但至少不会挂分,最后能拿到的分数也还能看。但是一上点难度,或者不对胃口,就明显感觉力不从心。总是只能打最低档的暴力,甚至有些题只能拿\(10pts\)的\(dfs\)分。有优化想不出来,有式子也推不出来。时间也总是不够用——在某道题上浪费了太多时间,最后刚刚推出一点接近正解(或者更高分的暴力)的结论就要考试结束了。
真是菜的令人发指……= =
讲实话,我从来不是什么很强的选手,甚至可能就中下而已。每场考试也不奢望能想到什么正解,只求能多拿一点分是一点分,把自己能想到的暴力稳稳当当地敲出来,不要犯什么低级错误就谢天谢地(其实以我的水平,也没有什么资本犯低级错误吧(苦笑),运气好的话,能推出一些特殊性质的部分分(然后再超常发挥地把它们敲出来?),不求考得多好,只求不要挂太难看……
最后这么几天攻难题似乎也没什么太大意义,把任务计划清理一下,然后打一打之前一直不熟悉的模板。一直逃避的高精,死活想不到状态的状压,因为已经死了所以没怎么用过的SPFA,没在考场上打过的主席树和平衡树,还有因为考的不多根本没怎么写过的字符串算法……至于剩下的,也只能顺其自然了。
懒得放题面了,自己找\(pdf\)吧
T1
我们知道\(string\)的排序是按字典序来比较的,这决定了在一堆字符串排完序之后两个字符串越接近,公共前缀长度就越长(即他们越相似),那么直接排完序之后每次抓一个不在自己位置上的,和它该在的位置上的字符串交换一下并记录方案即可
代码:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#pragma GCC optimize(2)
#define N (1000000 + 10)
using namespace std;
int n, cnt;
int rk[N];
string s[N];
int pos[N];
struct node2 {
int x, y;
}ope[N];
inline bool cmp (int a, int b) { return s[a] < s[b];}
int main() {
scanf("%d", &n);
for (register int i = 1; i <= n; ++i) cin >> s[i], pos[i] = i;
sort (pos + 1, pos + n + 1, cmp);
for (register int i = 1; i <= n; ++i) rk[pos[i]] = i;
for (register int i = 1; i <= n; ++i) {
if (pos[i] != i) {
ope[++cnt].x = pos[i], ope[cnt].y = i, swap(rk[pos[i]], rk[i]), swap(pos[rk[pos[i]]], pos[i]);
}
}
cout << cnt << '\n';
for (register int i = 1; i <= cnt; ++i) cout << ope[i].x << " " << ope[i].y << '\n';
return 0;
}
T2
二分+主席树验证
此题的\(trick\)有学习意义,\(kma\)太菜了并没有见过
暴力分可以贪心,发现显然只有最大的一个对答案有影响,所以每次抓最大的来减即可
正解
二分一下最终答案\(len\),每秒减少的值并不是问题,在\(check\)的时候把这个量加上去就行
考虑\(check\)在\(s\)次以内能不能把所有小麦值操作到\(len\)以下
首先我们需要\(check\)的只有\(a_i > len\)的,因为剩下的对答案没影响
那么我们最终的操作次数\(times = \sum\limits_{a_i > len}\lceil\frac{a_i - len}{x}\rceil\)
发现这个式子并不好维护到单次查询\(O(logn)\)的复杂度,也不能直接强拆,考虑巧妙地拆开分别维护
定义\({a}\)表示实数\(a\)的小数部分
将上式变形得到
对于每一坨,我们看看都能怎么搞
\(\sum\limits_{a_i < len} \lfloor\frac{a_i}{x}\rfloor\)在\(a_i\)排序之后是一个后缀和的形式,预处理之后每次询问二分找一下这个点,\(O(logn)\)
\(\sum\limits_{a_i > len} \lfloor\frac{len}{x} \rfloor\)是一个常量乘上累加的个数,假设上面二分找到的端点是\(k\),那么它等价于\((n - k + 1) * \lfloor\frac{len}{x}\rfloor\),\(O(1)\)
\(\sum\limits_{a_i < len}[(a_i\ mod \ x) > (len\ mod\ x)]\),发现这实际上是一个后缀意义上的区间查找某一个数的\(rank\),对\(a_i\ mod\ x\)离散化一下建一棵主席树之后在上面查询\(len\ mod\ x\)的排名即可,\(O(logn)\)
容易发现对于每次二分的\(check\),我们做到了\(O(logn)\)的复杂度,再加上前面的排序离散化之类的预处理,总复杂度为\(O(nlogn + mlog^2n)\)
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read() {
ll cnt = 0, f = 1; char c = getchar();
while (!isdigit(c)) {if (c == '-') f = -f; c = getchar();}
while (isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + (c ^ 48); c = getchar();}
return cnt * f;
}
const int N = (int)1e5 + 5;
int n, m, rt[N];
ll x, t[N], sum[N], b[N], sz, cnt, lim, s, nx;
void pre_work() {
sort (b + 1, b + sz + 1);
sz = unique(b + 1, b + sz + 1) - b - 1;
}
struct node {
int l, r;
int sum;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define sum(p) tree[p].sum
}tree[N * 32];
inline int _copy (int u) {
int v = ++cnt;
l(v) = l(u), r(v) = r(u);
sum(v) = sum(u); return v;
}
void Insert(int &p, int l, int r, int pos) {
p = _copy(p); ++sum(p);
if (l == r) return;
int mid = (l + r) >> 1;
if (pos <= mid) Insert(l(p), l, mid, pos);
else Insert(r(p), mid + 1, r, pos);
}
int query(int pos, int l, int r, int x) {
if (!pos||l >= x) return sum(pos);
int mid = (l + r) >> 1;
if (x <= mid) return sum(r(pos)) + query(l(pos), l, mid, x);
return query(r(pos), mid + 1, r, x);
}
bool ck(ll len, ll s) { //check - s
ll tot = 0; len += s;
int k = upper_bound(t + 1, t + n + 1, len) - t;
if (k > n) return true;
tot = sum[k]; tot -= (n - k + 1) * (len / x);
len = lower_bound(b + 1, b + sz + 1, len % x + 1) - b;
// if (tot < len) return true;
if (len <= sz) tot += (ll)query(rt[k], 1, sz, len);
return tot <= s;
}
ll binary(ll l, ll r) {
while (l < r) {
ll mid = (l + r) >> 1;
if (ck(mid, s)) r = mid;
else l = mid + 1;
}
return l;
}
int main() {
// freopen("bone.in", "r", stdin);
// freopen("bone.out", "w", stdout);
n = read(), m = read(), x = read();
for (register int i = 1; i <= n; ++i) t[i] = read(), lim = max(t[i], lim);
for (register int i = 1; i <= n; ++i) b[++sz] = t[i] % x;
pre_work();
sort (t + 1, t + n + 1);
for (register int i = n; i >= 1; --i) sum[i] = t[i] / x + sum[i + 1];
for (register int i = n; i >= 1; --i) {
ll gx = t[i] % x;
rt[i] = rt[i + 1];
gx = lower_bound(b + 1, b + sz + 1, gx) - b;
Insert(rt[i], 1, sz, gx);
}
for (register int i = 1; i <= m; ++i) s = read(), printf("%lld\n", binary(0, lim - s));
return 0;
}
T3
组合计数
总共有\(n-1\)个位置可以填+
对于每个\(a_i\)的贡献分别计算,考虑\(a_i\)之后离它最近的+
在哪里,因为这决定了\(a_i\)最后对于答案的贡献需要给\(a_i\)乘上一个\(base\)的多少次方(\(10^i\))
如果+
在\(a_i\)之后, 那么还剩下\(n - 2\)个空位可以填,还剩下\(m - 1\)个+
号,\(a_i\)的贡献是\(a_i * C_{n - 2}^{m - 1}\)
如果+
在\(a_{i + 1}\)之后,那么还剩下\(n - 3\)个空位可以填,还剩下\(m - 1\)个+
号,\(a_i\)的贡献是\(a_i * 10^{1} * C_{n - 3}^{m - 1}\)
\(\cdots\)
如果+
在\(a_{i + n - m - 1}\)之后,那么还剩下\(m - 1\)个空位可以填,还剩下\(m - 1\)个+
号,\(a_i\)的贡献是\(a_i * 10^{n - m + 1} * C_{m - 1}^{m - 1}\)
\(a_{i +n - m - 1}\)之后是能放的最后一个位置,因为如果再往后的话,其他+
会有堆在一起或出现在开头结尾的情况(空位不够)
对于+
在\(a_n\)之后的情况单独讨论,这种相当于\(a_i\)存在于最后一个整数中,那么还剩\(i - 1\)个空位可以填,\(m\)个+
,此时\(a_i\)的贡献是\(a_i * 10^{n - i} * C_{i - 1}^m\)
全部求和,暴力的复杂度是\(O(n^2)\)
发现对于每个乘\(10^k\)的\(a_i\),它们要乘的\(10^k\)是一样的,并且它们是连续的,考虑前缀和优化到\(O(n)\)
最后的柿子是$$\sum\limits_{i = 1}^{n - m}10^i * (\sum\limits_{j = 1}^{n - i} a_j * C_{n - i - 1}^{m - 1} + a_{n - i + 1} * C_{n - i}^m)$$
其中\(\sum\limits_{j = 1}^{n - i}a_j\)需要处理一个前缀和
#include<bits/stdc++.h>
#define ll long long
#define int ll
using namespace std;
inline int read() {
int cnt = 0, f = 1; char c = getchar();
while (!isdigit(c)) {if (c == '-') f = -f; c = getchar();}
while (isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + (c ^ 48); c = getchar();}
return cnt * f;
}
const ll mod = 998244353;
const int N = (int)1e6 + 5;
int n, m;
char x;
ll sum[N], fac[N], inv[N], a[N], ans;
ll mul (ll a, ll b) {return (a % mod * (b % mod)) % mod;}
ll add (ll a, ll b) {return a + b > mod ? a + b - mod : a + b;}
ll qpow(ll a, ll b) {ll c = 1; for (; b; b >>= 1, a = mul(a, a)) if (b & 1) c = mul(c, a); return c;}
void pre_work() {
fac[0] = 1, fac[1] = 1;
for (register int i = 2; i <= n; ++i) fac[i] = mul(fac[i - 1], (ll)i); inv[n] = qpow(fac[n], mod - 2);
for (register int i = n - 1; i >= 1; --i) inv[i] = mul(inv[i + 1], i + 1); inv[0] = 1;
}
ll C(ll m, ll n) {return mul(mul(fac[n], inv[m]), inv[n - m]);}
signed main() {
n = read(), m = read();
for (register int i = 1; i <= n; ++i) cin >> x, a[i] = (x ^ 48), sum[i] = add(a[i], sum[i - 1]);
pre_work();
for (register int i = 1; i <= n - m; ++i) {
ans = add(mul(qpow(10, i - 1), add(mul(sum[n - i], C(m - 1, n - i - 1)), mul(a[n - i + 1], C(m, n - i)))), ans);
}
printf("%lld", ans);
return 0;
}