欸我去出题人怎么是go批,欸我去mygo还在追我
A. Tokitsukaze and Absolute Expectation
题意
对于一个长为 的序列
,第
个元素
的值将会从
中独立等概率生成。定义:
求 的期望。
思路
首先,因为每个元素值生成的事件是相互独立的,所以根据期望的可加性,将式子化简如下:
对于 ,记
,并钦定
。那么分母是很显然的,为两个区间的长度之积,接下来我们考虑如何求分子。
可以发现,期望的计算涉及相交的区间和不相交的区间。记 ,那么我们可以将区间拆分为两两相交的
,以及两两不相交的
和
。
我们先来考虑简单的不相交。对于两个不相交的区间 ,假设我们在第二个区间选择了左端点
,那么由第一个区间所有选择构成的答案序列满足公差为
的等差数列,首项为
,项数为第一个区间的长度。而当我们在第二个区间选择
时,其距离第一个区间的所有点都增大了
,所以对应地答案是原来的基础上增加一倍第一个区间的长度。以此类推,可以发现增大的倍数构成一个公差为
的等差数列,首项为
,项数为
,因而求得式子。
剩下的就是相交的情况,也就是 。虽然注意力没那么好,但是打表发现貌似有规律。丢进 OEIS 发现确实有公式,即
,这里丢一个链接:A007290 - OEIS。
那么本题就做完了,不过注意一些边界的判断。
时间复杂度:
对应AC代码
constexpr int P = 1e9 + 7;
using Z = MInt<P>;
void solve() {
int n;
cin >> n;
vector<int> l(n + 1), r(n + 1);
for(int i=1;i<=n;i++) cin >> l[i] >> r[i];
auto cal1 = [&](int l1, int r1, int l2, int r2) -> Z {
return Z(l2 - r1 + l2 - l1) * (r1 - l1 + 1) * (r2 - l2 + 1) / 2 + Z(r1 - l1 + 1) * (r2 - l2 + 1) * (r2 - l2) / 2;
};
auto cal2 = [&](int l, int r) -> Z {
int len = r - l + 1;
return Z(len + 1) * len * (len - 1) / 3;
};
Z ans = 0;
for(int i=2;i<=n;i++) {
int l1 = l[i - 1], r1 = r[i - 1], l2 = l[i], r2 = r[i];
if(l1 > l2) swap(l1, l2), swap(r1, r2);
int x = max(l1, l2), y = min(r1, r2);
Z now = 0;
if(x > y) now += cal1(l1, r1, l2, r2);
else {
if(l1 != l2) now += cal1(l1, x - 1, l2, r2);
if(r1 != r2) now += cal1(x, y, y + 1, max(r1, r2));
now += cal2(x, y);
}
ans += now / (r1 - l1 + 1) / (r2 - l2 + 1);
}
cout << ans << '\n';
}
注意到我没有注意到,所以我没注意到
B. Tokitsukaze and Balance String (easy)
详见
,区别是本题可以暴力
C. Tokitsukaze and Balance String (hard)
题意
定义当一个字符串满足连续子串中 和
的个数相等时,该字符串是平衡的。
定义一个字符串 的
为满足将
翻转后字符串变为平衡的下标
的个数。
给定一个由 组成的字符串,
可以被替换为
,输出所有替换方案对应的
之和。
思路
首先,替换完成后我们会得到一个二进制串,只包含两种字符,所以除非 后面没有
,不然一定会出现一个与之对应的
。反过来也是一样的。
所以说,我们其实只需关注头尾两个字符的情况,而中间是无所谓的,因为可以被抵消。而若要让字符串平衡,我们只能让头尾两个字符相等。所以如果 中包含
个
,那么这些位置都是无所谓的,我们在完成后续计算后乘上
即可。
上述结论对于替换操作来说,替换 中的字符对平衡性毫无影响,反之能反转平衡性。所以对于一个平衡字符串,
,否则
。
然后我们针对开头和结尾是否为 进行分讨即可。如果都不满足,那么我们根据平衡性即可得到答案;否则,我们就可以根据不同的选择得到不同的平衡性。
时间复杂度:
对应AC代码
void solve() {
int n;
cin >> n;
string s;
cin >> s;
if(n == 1) {
cout << (s[0] == '?' ? 2 : 1) << '\n';
return;
}
int ans = 1;
for(int i=1;i<n-1;i++) {
if(s[i] == '?') ans = ans * 2 % mod;
}
if(s[0] != '?' && s[n - 1] != '?') {
if(s[0] != s[n - 1]) ans *= 2;
else ans *= n - 2;
}else {
if(s[0] == s[n - 1]) ans *= 2;
ans *= n;
}
cout << ans % mod << '\n';
}
很思维
D. Tokitsukaze and Concatenate Palindrome
题意
给定一个长为 的字符串
和一个长为
的字符串
,定义一次操作为选择任意字符串的任意位置,并将其替换为任意字符。输出使得两个字符串任意排列后拼接得到回文串的最小操作数。
思路
首先,钦定 更长,便于后面的讨论。
因为需要操作数尽可能小,显然我们可以先把两者的相同字符排列在两端,然后考虑剩余的字符。
如果我们有 个多余的字符,并想让其排列后构成回文串,那么我们只需修改其中的
个字符即可,所以我们现在的目标是求得
。
考虑拼接后的样子,剩下的大概是 剩下的字符,
中的最多
对字符(对称放在中间),
剩下的字符。
所以我们只需处理中间的配对。不过可能会出现拼接后为奇数长度的情况,此时需要将 减去
,避免重复计算。
时间复杂度:
对应AC代码
void solve() {
int n, m;
cin >> n >> m;
string a, b;
cin >> a >> b;
if(n > m) swap(n, m), swap(a, b);
int ans = 0;
vector<int> cnt(26);
for(auto &it : b) cnt[it - 'a'] ++;
for(auto &it : a) {
if(cnt[it - 'a']) cnt[it - 'a'] --;
else ans ++;
}
int p = 0, q = 0; //(m - n) / 2 * 2
for(auto &it : cnt) p += it, q += it / 2;
if((n + m) % 2 == 1 && p > 0) p --;
ans += p - min(q, (m - n) / 2) * 2;
cout << ans / 2 << '\n';
}
细节太多了
E. Tokitsukaze and Dragon's Breath
题意
给定一个 的矩阵,定义一个格子
的贡献为以这个格子为中心向
一直延伸得到的 "X" 形区域内格子的值的总和,求单个格子的最大贡献。
思路
我们可以将矩阵视为平面直角坐标系,以 为原点,并将格子全都视为坐标点,那么可以发现点全都位于
直线上,且每个作为中心的格子延伸得到的区域恰好对应其中的两条直线。
所以我们将两类直线分开考虑,以 作为偏移量,记录该偏移量所对应的直线上的点权总和。然后就只需枚举每个点作为中心,将两条直线的总和加起来,再减掉重叠区域,也就是中心即可。
时间复杂度:
对应AC代码
void solve() {
int n, m;
cin >> n >> m;
vector<vector<int>> a(n + 1, vector<int>(m + 1));
map<int, int> p, q;
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
cin >> a[i][j];
p[i + j] += a[i][j], q[i - j] += a[i][j];
}
}
int ans = 0;
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
ans = max(ans, p[i + j] + q[i - j] - a[i][j]);
}
}
cout << ans << '\n';
}
经典 map 题
F. Tokitsukaze and Kth Problem (easy)
题意
给定一个长为 的序列
,定义
。给定一个整数
,输出
的所有前
大,不存在输出
。
思路
首先,因为数组位置对答案没有影响,所以我们可以先将数组取模,然后排序。其次,因为这是两个项相加,所以值域最多只会到 。
针对前 大,我们可以先二分出第
大的大小
,然后再根据
找答案。
函数可以用双指针实现,或者二分里面套二分。
因为值域可能会跑到 里去,所以我们可以记
为满足
的
数量,那么总数量就是
。
除此之外还有一个坑,极端情况下数组可能由一种数字组成,这可能会导致求最后的数组的时候复杂度爆炸。所以我们可以求出大于 的所有答案,然后用
填充。
时间复杂度:
对应AC代码
void solve() {
int n, p, k;
cin >> n >> p >> k;
vector<int> a(n + 1);
for(int i=1;i<=n;i++) {
cin >> a[i];
a[i] %= p;
}
sort(a.begin(), a.end());
auto sum = [&](int x) -> int {
int cnt = 0;
for(int i=1,j=n;i<j;i++) {
while(j > 0 && a[i] + a[j] >= x) j --;
if(i >= j) break;
cnt += j - i;
}
return n * (n - 1) / 2 - cnt;
};
auto check = [&](int x) -> bool {
return sum(x) - sum(p) + sum(p + x) >= k;
};
int l = 0, r = p - 1, mid;
while(l <= r) {
mid = (l + r) >> 1ll;
if(check(mid)) l = mid + 1;
else r = mid - 1;
}
vector<int> ans;
for(int i=1,j=n,t=n;i<=n;i++) {
while(j > 0 && a[i] + a[j] >= l) j --;
while(t > 0 && a[i] + a[t] >= l + p) t --;
for(int x=max(i,j)+1;x<=n;x++) {
if(a[i] + a[x] >= p) break;
ans.emplace_back(a[i] + a[x]);
}
for(int x=max(i,t)+1;x<=n;x++) {
ans.emplace_back(a[i] + a[x] - p);
}
}
sort(ans.begin(), ans.end(), greater<>());
for(auto &it : ans) cout << it << ' ';
k -= ans.size();
while(k --) cout << l - 1 << ' ';
cout << '\n';
}
二分写麻了,参考了一下出题人题解
G. Tokitsukaze and Kth Problem (hard)
待补充
H. Tokitsukaze and Necklace
待补充
I. Tokitsukaze and Pajama Party
题意
给定一个字符串 ,对于模式串
,
代表匹配至少一个任意字符,输出
中能匹配该模式串的子串数量。
思路
我们将模式串看成两部分,即 和
。当我们枚举到位置
时,如果
开始的子串能匹配上模式串的右半部分,那么我们就只需知道
中有多少
能作为模式串的左半部分,这些
可以和我们当前枚举到的右半部分组成模式串。
时间复杂度:
对应AC代码
void solve() {
int n;
cin >> n;
string s;
cin >> s;
string p = "uwawauwa";
int m = p.size();
int ans = 0, cnt = 0;
for(int i=0;i+m-1<n;i++) {
bool ok = true;
for(int j=0;j<m;j++) {
if(s[i + j] != p[j]) ok = false;
}
if(ok) {
ans += cnt;
if(i > 0 && s[i - 1] == 'u') ans --;
}
if(s[i] == 'u') cnt ++;
}
cout << ans << '\n';
}
兄弟
J. Tokitsukaze and Recall
题意
给定一个不一定连通的无向图,以及一个整数 。提供
次操作,一次操作为选择一个点并占领。可以从占领的任何位置通过边占领另一个位置。输出字典序最小的占领顺序。
思路
首先,我们可以用并查集找出所有连通块,并求出连通块中点的个数和编号最小的点。然后我们按照点的个数降序排序,个数相同时按照编号升序排序,得到若干个排序后的连通块,将前 个连通块中编号最小的点放入优先队列。剩下的就是遍历优先队列,拿出队列里最小的元素,并将其占领,然后枚举其儿子,放入队列即可。
但是当 很大时,这么做存在一个问题:我们可以用多余的
先占领某些编号小的点,不然
就浪费了。
不妨考虑贪心,只要 还有剩余,我们一定会按照
顺序占领,所以我们只需在每次遍历队列前,判断队列的最小值是否过大,是则消耗
然后放入当前能放入的最小值。
时间复杂度:
对应AC代码
struct DSU {
vector<int> pa, siz;
void init(int n) { pa = vector<int>(n + 1), siz = vector<int>(n + 1, 1), iota(pa.begin(), pa.end(), 0); }
int find(int x) { while(x != pa[x]) x = pa[x] = pa[pa[x]]; return x; }
bool unite(int u, int v) {
int f1 = find(u), f2 = find(v);
if (f1 == f2) return false;
siz[f1] += siz[f2];
pa[f2] = f1;
return true;
}
int size(int x){ return siz[find(x)]; }
}dsu;
void solve() {
int n, m, k;
cin >> n >> m >> k;
vector<set<int>> adj(n + 1);
dsu.init(n);
while (m --) {
int u, v;
cin >> u >> v;
adj[u].emplace(v), adj[v].emplace(u);
dsu.unite(u, v);
}
vector<int> mn(n + 1, inf), cnt(n + 1);
for(int i=1;i<=n;i++) {
int pa = dsu.find(i);
if(mn[pa] == inf) mn[pa] = i;
cnt[pa] ++;
}
vector<pair<int, int>> g;
for(int i=1;i<=n;i++) {
if(cnt[i] > 0) g.emplace_back(-cnt[i], mn[i]);
}
sort(g.begin(), g.end());
priority_queue<int, vector<int>, greater<>> q;
for(int i=0;i<min((int)g.size(),k);i++) q.emplace(g[i].second);
k -= min((int)g.size(), k);
vector<int> st(n + 1), ans;
int now = 1;
while(!q.empty()) {
if(now < q.top() && k > 0) {
q.emplace(now);
k --;
}
int x = q.top();
q.pop();
if(st[x]) continue;
st[x] = true;
ans.emplace_back(x);
if(x == now) now ++;
for(auto &y : adj[x]) {
if(st[y]) continue;
q.emplace(y);
}
}
cout << ans.size() << '\n';
for(auto &it : ans) cout << it << ' ';
cout << '\n';
}
写起来麻烦了点,但不知道为啥过的人这么少
K. Tokitsukaze and Shawarma
题意
三个东西,分别需要 个,做一个分别需要
分钟,不同东西可以并行制作,计算至少需要多久做完。
思路
这能有什么思路。
时间复杂度:
对应AC代码
void solve() {
int x, y, z, a, b, c;
cin >> x >> y >> z >> a >> b >> c;
cout << max({a * x, b * y, c * z}) << '\n';
}
没事至少不是Hello World
L. Tokitsukaze and XOR-Triangle
题意
给定两个长为 的数组
,对于
次询问,每次询问给定一个区间
,求:
思路
首先,我们来考虑一下下面的式子怎么求:
位运算一般来说拆位做会很方便,因为会出现一些奇妙的性质。假设 为
的第
个二进制位,从
开始计数,
同理,那么式子变成了下面这样:
假设我们有一个由 和
构成的长为
的二进制序列
,将其全都异或上
相当于翻转二进制值,
则是不变。而一个二进制序列的总和等于
的个数,我们记为
,那么全都异或上
后总和变为
,
则是不变。
那么将上面的操作应用到这个式子上,我们相当于将 中
子序列视为
,然后枚举
中
区间内的每个元素,分别求出
全都异或上这个元素的值得到的总和,最后进行求和。
因为 也是二进制序列,所以针对
的操作只有两种,
和
。所以我们只需分别求出
内
和
中
的个数,经过简单的计算即可得到结果,而前者用前缀和即可实现。
好的,现在我们回到原问题。直接求会很麻烦,但如果假设 ,我们就可以进行递推了。记:
显然有:
上面的求和可以用后缀和与 是否为
来快速计算,具体方法在上面已经提到了。
而有趣的是,题目的式子可以用这个进行化简:
式子的左半部分即为 ,类似于前缀和,而右边按照我们最开始考虑的式子的解法做即可。
时间复杂度:
对应AC代码
constexpr int P = 1e9 + 7;
using Z = MInt<P>;
void solve() {
int n, q;
cin >> n >> q;
vector<int> a(n + 1), b(n + 1);
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++) cin >> b[i];
vector<vector<Z>> pre_a(n + 1, vector<Z>(31)), pre_b(n + 1, vector<Z>(31));
vector<vector<Z>> f(n + 2, vector<Z>(31));
auto ret = [&](int t, int il, int ir, int jl, int jr) -> Z {
Z cnt_a = pre_a[ir][t] - pre_a[il - 1][t], cnt_b = pre_b[jr][t] - pre_b[jl - 1][t];
return cnt_a * (jr - jl + 1 - cnt_b) + (ir - il + 1 - cnt_a) * cnt_b;
};
for(int t=0;t<=30;t++) {
for(int i=1;i<=n;i++) pre_a[i][t] = pre_a[i - 1][t] + (a[i] >> t & 1ll);
for(int i=1;i<=n;i++) pre_b[i][t] = pre_b[i - 1][t] + (b[i] >> t & 1ll);
for(int i=n;i>=1;i--) {
f[i][t] = f[i + 1][t];
if(a[i] >> t & 1ll) f[i][t] += n - i + 1 - (pre_b[n][t] - pre_b[i - 1][t]);
else f[i][t] += pre_b[n][t] - pre_b[i - 1][t];
}
}
while(q --) {
int l, r;
cin >> l >> r;
Z ans = 0;
for(int t=0;t<=30;t++) {
Z now = f[l][t] - f[r + 1][t] - ret(t, l, r, r + 1, n);
ans += (1ll << t) * now;
}
cout << ans << '\n';
}
}
推式子题