Processing math: 100%

Codeforces Round 868 Div 2

A. A-characteristic (CF 1823 A)

题目大意

要求构造一个仅包含11的长度为 n的数组 a,使得存在 k个下标对 (i,j),i<j满足 ai×aj=1

解题思路

当有x1y1时,其满足条件的下标对数量为 x(x1)2+y(y1)2

由于n只有 100,直接枚举 x即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t--){
int n, k;
cin >> n >> k;
int one = 0;
for(; one <= n; ++ one){
int neg = n - one;
if (neg * (neg - 1) + one * (one - 1) == 2 * k)
break;
}
if (one > n)
cout << "No" << '\n';
else{
cout << "Yes" << '\n';
for(int i = 0; i < one; ++ i)
cout << 1 << ' ';
for(int i = 0; i < n - one; ++ i)
cout << -1 << ' ';
cout << '\n';
}
}
return 0;
}


B. Sort with Step (CF 1823 B)

题目大意

给定一个排序,问能否仅通过交换相隔k的俩元素,使得有序。不能的话问能否事先通过一次任意交换操作后,再通过之前的操作交换得到有序。

解题思路

考虑每个元素的原始位置和最后所在的位置,它们对k的取模应该相同。否则就不能有序。

而如果恰好有两个元素其对k的取模不同,且交换之后是相同的,则可以。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t--){
int n, k;
cin >> n >> k;
vector<int> a(n);
for(auto &i : a){
cin >> i;
i --;
}
bool ok1 = true;
map<pair<int, int>, int> cnt;
for(int i = 0; i < n; ++ i){
if (i % k != a[i] % k){
ok1 = false;
cnt[{min(i % k, a[i] % k), max(i % k, a[i] % k)}] ++;
}
}
if (ok1)
cout << 0 << '\n';
else if (cnt.size() == 1 && cnt.begin()->second == 2)
cout << 1 << '\n';
else
cout << -1 << '\n';
}
return 0;
}


C. Strongly Composite (CF 1823 C)

题目大意

给定一个数组a,构造数组 b,要求最大化数组的元素数量,使得俩数组的所有元素的乘积相同,且数组 b的每个数都是强合数

强合数的定义为,合数因子数量质数因子数量。

解题思路

乘积相同,相当于将数组a里的质数重新组合;数量最大,相当于尽可能少用质数来组成一个新的数。

可以发现,两个相同的质数组成一个强合数,或者三个不同的质数可以组成一个强合数。

由此我们统计数组 a中的每个质数的数量,同个质数俩俩组合,不同质数三三组合,就能最大化答案了。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t--){
int n;
cin >> n;
map<int, int> cnt;
auto fac = [&](int a){
for(int i = 2; i * i <= a; ++ i){
while (a % i == 0){
cnt[i] ++;
a /= i;
}
}
if (a != 1)
cnt[a] ++;
};
for(int i = 0; i < n; ++ i){
int a;
cin >> a;
fac(a);
}
LL ans = 0;
int left = 0;
for(auto &[_, value] : cnt){
ans += value / 2;
left += (value & 1);
}
ans += left / 3;
cout << ans << '\n';
}
return 0;
}


D. Unique Palindromes (CF 1823 D)

题目大意

要求构造一个仅包含小些字母的字符串s,长度为n,且满足 k个限制。

每个限制表述为(xi,ci), 字符串s的长度为 xi的前缀满足有 ci个本质不同的回文串)

解题思路

通过打表发现本质不同的回文串数量不会超过字符串长度。

注意到k最大只有 20,这启示我们每个限制可以用一个字符去满足。

思考朴素的构造方法,对于一个长度为 n的字符串,我们可以 aaaaaaaabcabcabc这样去构造,一开始连续的 a的数量就能控制这个字符串的本质不同的回文串的数量。这样的构造方法满足其数量在 [3,n]之内,这刚好符合题意里 ci3的限制。

因此我们可以先根据第一个限制构造出如上的字符串,对于之后的限制进行增量构造,增加的回文数量用 ddddeeeee这样构造,剩下的长度用 abc这样不会增加回文串数量的形式去填充。

注意用于填充的字符串,在每次填充时应该继续前面的,而不是从头(从abc )开始(如代码的fill_cur),不然可能会新增回文串。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--){
int n, k;
cin >> n >> k;
vector<int> x(k), c(k);
for(auto &i : x)
cin >> i;
for(auto &i : c)
cin >> i;
string ans;
int fill_cur = 0;
auto fill = [&](int x){
while(x--){
ans.push_back('a' + fill_cur);
fill_cur = (fill_cur + 1) % 3;
}
};
auto ok = [&](){
int cur = 0;
for(int i = 0; i < k; ++ i){
int dis = x[i] - c[i];
if (dis < 0)
return false;
if (cur > dis)
return false;
cur = dis;
}
ans += string(c[0] - 3, 'a');
fill(x[0] - ans.size());
for(int i = 1; i < k; ++ i){
ans += string(c[i] - c[i - 1], 'c' + i);
fill(x[i] - ans.size());
}
return true;
};
if (ok()){
cout << "YES" << '\n';
cout << ans << '\n';
}else {
cout << "NO" << '\n';
}
}
return 0;
}


E. Removing Graph (CF 1823 E)

题目大意

两人博弈。

给定n个环,每个人可以从[l,r] 中选一个数x,然后选择由x 个点组成的连通子图,将点及其边去掉。不能操作者输。

在绝顶聪明的情况下,问先后手谁必赢。

解题思路

每个环都是一个独立局面,因此我们求出每个环的sg值,异或起来,非零就先手赢,否则后手赢。

对于一个环来说,取了一次之后就变成一条链了。因此一个环的 sg值就是所有可能的链的长度对应的sg值的 mex

对于一个链来说,取了一次之后就变成两条链,这两条链分别都是一个独立局面,因此一个链的 sg值,就是一些操作值的 mex, 而操作值就是取了之后(有取的长度取的位置两个因素)的两个链的sg值的异或。

abc287gabc297g就是要求一个链的sg值。

注意到题目保证了 lr,对于一条链来说,如果它能取(即长度 l),则必定能分成两条长度一样的链,之后先手就模仿后手的操作,就能必赢了。

也就是说,对于一个环来说,如果其长度lenl+r,那么先手取了一次后,变成的链因为后手必定可以再取(lenrl ),所以对于后手来说必定是个必胜态,所以这样的环对于先手来说必定是个必败态,其 sg值为 0

而长度小于 l,不能取,那肯定是必败态,其 sg值为 0

考虑环长度 在 [l,l+r)之间的sg值,其对应的链长度有 lenl,lenl1,lenl2,...,lenr。其sg值就是这些可能的链长度的 sg值取 mex

考虑链长度,小于 l,是必败态,其 sg值为 0。 而sg(l)=sg(l+1)=sg(l+2)...=sg(l+l1)=1

sg(2l)=mex(sg(l),sg(l1),sg(l2),...,sg(1),sg(0))=mex(1,0,0,0,...,0)=2=sg(2l+1)=sg(2l+2)

sg(3l)=mex(sg(2l),sg(2l1),...,sg(l),sg(l1),...,sg(0))=mex(2,1,...,1,0,...,0)=3=sg(3l+1)=sg(3l+2)

上述的mex里的每一项都是取最边边的结果(即取了之后还有一个链),至于有两条链的结果,是长度更小的两个链的sg的异或值,其不会超过上面的最大值。

由此(或打表)可以发现长度为sg(len)=lenl(llen<l+r)

进而环的 sgc(len)=mex(sg(lenl),sg(lenl1),...,sg(lenr))=lenl(llen<l+r)

环的sg值求出来了,异或一下就知道谁赢了。

至于环大小,用并查集或BFS一下就知道了。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> sz;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
sz.resize(n);
iota(p.begin(), p.end(), 0);
fill(sz.begin(), sz.end(), 1);
}
inline int get(int x) {
return (x == p[x] ? x : (p[x] = get(p[x])));
}
inline bool unite(int x, int y) {
x = get(x);
y = get(y);
if (x != y) {
p[x] = y;
sz[y] += sz[x];
return true;
}
return false;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, l, r;
cin >> n >> l >> r;
dsu d(n);
for(int i = 0; i < n; ++ i){
int u, v;
cin >> u >> v;
-- u;
-- v;
d.unite(u, v);
}
int ans = 0;
for(int i = 0; i < n; ++ i){
if (d.get(i) == i){
if (d.sz[i] >= l && d.sz[i] < l + r)
ans ^= d.sz[i] / l;
}
}
if (ans)
cout << "Alice" << '\n';
else
cout << "Bob" << '\n';
return 0;
}


F. Random Walk (CF 1823 F)

题目大意

树上随机游走,从点s到点 t,问每个点访问次数的期望值。

解题思路

每次的期望题感觉都比较神仙。

注意到这是棵树,点s到点t的路径是唯一的,设路径为s,u0,u1,...,uk,t

一开始设状态dp[s][v][t]表示从 s点到 t点,期望访问到 v号点的次数,然后枚举到相邻点的状态,即dp[s][v][t]=(s,u)Edp[u][v][t],但感觉怎么都算不出来。

然后想着从点 s出发,它可以往很多个相邻点走,只有一个点u0是更接近点 t的, 且最终到点t时立刻停下来,这意味着点t之后的点的访问次数的期望值一定是 0

考虑到一旦走到点u0时,发现问题貌似变成了一个子问题了,可以认为是从点u0出发,到点 t的情况。换句话说,我们可以将 点s到点 t的步骤分成若干步,分别是点 s到点 u0,点 u0到点 u1... 点ut到点 t,由于期望的线性可加性,每个点的期望访问次数,可以由这些的每个步骤的影响依次累计。

dp[s][w]表示从 s 点往更接近点t的方向走(即走到 u0点),对 w点的期望访问次数。

设点 s的度数为 dus,其余字母定义看图,根据期望定义,可以得到:

tree

dp[s][w]=1dus×0+1dus(dp[x][w]+dp[s][w])+dus2dus(dp[y][w]+dp[s][w])

这里有三个部分:

  • 1dus的概率选择走到 u0,然后就停下来了,此时对 v的访问贡献是0
  • 1dus的概率往 w所在的子树走(即点 x),此时对w的访问贡献是由xssu0组成,即 dp[x][w]+dp[s][w]
  • dus2dus的概率往其他子树走(即点 y表示的其他所有点),此时对w的访问贡献是由yssu0组成,即 dp[y][w]+dp[s][w]。但由于从点y到点w必须经过点 s,而一旦到点 s就会停下来( dp[y][w]即表示从点 y到更接近 点t的方向走(即往点 s),对点 w的访问贡献),因此 dp[y][w]=0

这样上述式子移一下项,就得到

dp[s][w]=dp[x][w]

即点s往点 u0走时的对点w访问次数的贡献是等价于点 x往点 s走时,对点 w的贡献。由此就可以得到

dp[s][w]=dp[x][w]=dp[x1][w]=...=dp[xk][w]=dp[w][w]

剩下的就是求 dp[w][w]。根据期望定义,可以得到

dp[s][s]=1dus×1+dus1dus(dp[o][s]+dp[s][s])

这里有两部分:

  • 1dus的概率选择走到 u0,此时就停下来了,因此对s的访问贡献是1(一开始在s时的贡献)。
  • dus1dus的概率选择走到除点u0之外的其他点(设点o,即 xy),因此对s的访问贡献是osss,即dp[o][s]+dp[s][s],而因为点o到点 s就会停下来(点 s是更接近点 t的点),因此 dp[o][s]=1(一开始在s时的贡献包含在 dp[s][s]里)。

这样上述式子移一下项,就得到

dp[s][s]=dus

综合上述的两个式子dp[s][w]=dp[w][w]=duw,可以得出,每当进行一次 su0,u0u1,,ukt 时,其他点w的期望访问次数都会增加 duw,其中点w是点 s除了 u0方向的其他方向的点(见上图的虚线包括起来,就是对应颜色的箭头的影响)。

也就是说一个点a的期望访问次数就是dua×cnta,其中 cnta等于该点与路径st的交点(以上图为例,就为 uk1)到 t的点数(见上图的点a)。

剩下的就是如何求 cntw,我们可以以点 s为根,然后我们从点 t开始,一路沿着 父亲节点上去,就回到点s,其中每往父亲跳一次时, cntw就会加一,比如从tuk时, cnt=0+1=1,此时再遍历一下除了tuk1方向的所有点 w,它们的答案就是 duw×cnt

最终的时间复杂度是O(n)

虽然答案不会超过n2,但记得对998244353取模。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int mod = 998244353;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, s, t;
cin >> n >> s >> t;
-- s, -- t;
vector<vector<int>> edge(n);
vector<int> du(n);
for(int i = 1; i < n; ++ i){
int u, v;
cin >> u >> v;
-- u, -- v;
edge[u].push_back(v);
edge[v].push_back(u);
++ du[u];
++ du[v];
}
vector<int> fa(n);
function<void(int, int)> dfs = [&](int u, int f){
fa[u] = f;
for(auto &v : edge[u]){
if (v == f)
continue;
dfs(v, u);
}
};
dfs(s, s);
vector<int> ans(n);
int cnt = 1;
ans[t] = 1;
function<void(int, int, int)> dfs2 = [&](int u, int f, int cnt){
ans[u] = 1ll * du[u] * cnt % mod;
for(auto &v : edge[u]){
if (v == f)
continue;
dfs2(v, u, cnt);
}
};
do{
int cur = fa[t];
ans[cur] = 1ll * cnt * du[cur] % mod;
for(auto &u : edge[cur]){
if (u != fa[cur] && u != t)
dfs2(u, cur, cnt);
}
t = cur;
++ cnt;
}while(s != t);
for(auto &i : ans)
cout << i << ' ';
cout << '\n';
return 0;
}


posted @   ~Lanly~  阅读(164)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于决定:把自己家的能源管理系统开源了!
· [.NET] 使用客户端缓存提高API性能
· 外部H5唤起常用小程序链接规则整理
· C#实现 Winform 程序在系统托盘显示图标 & 开机自启动
· WPF 怎么利用behavior优雅的给一个Datagrid添加一个全选的功能
点击右上角即可分享
微信分享提示