曇天も払う光なら凄惨な旅路だって往け|

AzusidNya

园龄:1年7个月粉丝:4关注:4

Atcoder杂题笔记

大概会把博客当草稿纸用(
当然写出正解还是会把正解贴出来。


[ARC080E] Young Maids (待补代码)

给定正偶数 N

给定 N 元排列 p=(p1,p2,...,pN). Snuke 打算根据下述步骤构造一个 N 元排列 q

首先,令 q 为空。接下来,执行下述操作直到 p 为空。

  • 选择 p 中两个相邻元素 ,按原顺序设它们是 xy. 从 p 中移除 xy,将它们按顺序接在 q 的前面。

试求可能的形成的 q 中,字典序最小的排列。

note:

可以发现最终排列相邻两个数在原串相对位置固定。

观察样例大概能看出第一位只有可能是奇数位的。

考虑黑白染色,很明显在最终排列中对于任意 i2i1 位和 2i 位异色。

于是每次贪心的拿出最小的黑白数对,然后递归求解这个数对左边、右边、内部的答案,其中内部颜色需反转,将三个数列进行归并即可。

但是这样时间复杂度是错误的。

正解是这个的优化,利用 vector 的 swap 时间复杂度是 O(1) 的特性进行启发式合并。


[ARC076F] Exhausted?

m 个椅子在数轴上排列,第 i 张椅子的坐标为i。

高桥君和他的朋友一共有 n 个人。高桥君他们因为玩了太久的游戏,大家的腰和背都很痛,所以他们很有必要坐在椅子上休息一下。

高桥君他们每个人坐的椅子的坐标都很讲究,第 i 个人想坐在坐标在 li 以下(包括 li)的椅子上,或者坐在坐标在 ri 以上(包括 ri)的椅子上。当然,一个的椅子只能坐一个人。

可这样计算下去,可能会让他们不能都坐在椅子上休息。青木君关心高桥君他们的健康,尽可能多地增加椅子,让高桥君他们都能够坐在椅子上休息。 椅子可以添加到任意的实数坐标上,请求出需要添加椅子数量的最小值。

note:

忙猜这图论题。

将每个人和能坐的坐标连边后二分图的最大匹配就是答案。但是时间复杂度会炸裂。

(待补)


[ARC148E] ≥ K

给定长度为 n 的数列 {ai} 和一个自然数 K, 可以将 {ai} 打乱顺序重排,问多少种结果序列满足 i[1,n),ai+ai+1K。 答案对 998244353 取模。

note & solution:

好玩的计数题。

将数分成两类,大于或等于 k2 的和小于 k2 的。分类后我们发现,大于 k2 的数可以跟任何同类型数相邻,而小于 k2 的数不能与任何同类型数相邻。

而在不同类型的数中,两个数 xy(x>y) 可相邻的条件是 |k2x|>|k2y|

于是考虑按 |k2ai| 从大到小排序,对 ai 去重并记录出现次数,然后依次考虑插入 ai

仍然分类讨论插入 ai 的情况。维护当前可插入的空位数量 s

  1. 对于小于 k2 的数。先在当前可用的位置插入当前数。假设这个数有 cnt 个,则产生的贡献是 (scnt)。因为对 |k2ai| 排序了,所以后面的任何数都不能插入到当前数的旁边,因此可用的的空位数量会减少, sscnt。特别地,如果当前空位不够放置 cnt 个数,则无解。

  2. 对于大于 k2 的数,这种情况可以使用插板法计算贡献。贡献为(s+cnt1s1)。因为后面的数可以任意插入在当前数旁边,因此可用的空位数量会增加,ss+cnt

分类讨论并计算答案即可。需要注意排序时如果|k2x|=|k2y| 则将大于或等于 k2 的数放在前面,因为这两个数可以相邻放置。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<vector>
#define int long long
using namespace std;
const int modd = 998244353;
int n, k;
int frac[200005], inv[200005], a[200005], h[200005];
bool del[200005];
int ksm(int u, int v){
if(v == 0) return 1;
int ans = ksm(u, v >> 1);
ans = ans * ans % modd;
if(v & 1) ans = ans * u % modd;
return ans;
}
int abss(int u){ return u < 0 ? -u : u; }
bool cmp(int u, int v){ int x1 = abss(2 * u - k), y1 = abss(2 * v - k); return (x1 != y1 ? x1 > y1 : u > v); }
int getC(int u, int v){ return (((frac[u] * inv[v]) % modd) * inv[u - v]) % modd;}
int s = 1, ans = 1;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
frac[0] = 1, inv[0] = 1;
for(int i = 1; i <= n + 1; i ++)
frac[i] = (frac[i - 1] * i) % modd;
inv[n + 1] = ksm(frac[n + 1], modd - 2);
for(int i = n; i >= 1; i --)
inv[i] = inv[i + 1] * (i + 1) % modd;
for(int i = 1; i <= n; i ++)
cin >> a[i];
sort(a + 1, a + n + 1, cmp);
int cnt = 0, pos = 0;
a[0] = -1, a[n + 1] = -2;
for(int i = 1; i <= n + 1; i ++)
if(a[i] == a[pos]) cnt ++, del[i] = true;
else h[pos] = cnt, cnt = 1, pos = i;
for(int i = 1; i <= n; i ++){
if(del[i]) continue;
if(2 * a[i] - k >= 0)
(ans *= getC(s + h[i] - 1, s - 1)) %= modd, s += h[i];
else{
if(s < h[i]){ cout << 0; return 0;}
(ans *= getC(s, h[i])) %= modd, s -= h[i];
}
}
cout << ans;
return 0;
}

[ARC127E] Priority Queue

给定一个长度为 a+b 的序列 x,并且刚好有 a 个 1,b 个 2。

有一个集合 s,初始是空的,将会做 a+b 次操作,第 i 次操作如下:

  1. xi=1,选择一个数 v[1,a],并把这个数加入到集合中,这里 v 必须是之前没有选择过的数
  2. xi=2,将集合中最大的数删掉

问最后 s 能有几种状态。

note:

看起来还是从结果入手。

最终序列的长度是确定的,为 ba

考虑什么样的数列不可能成为最终答案。首先想到的是如果 x 的最后一列是 2,那么有 a 不可能出现在序列中。记序列最后一个 2 的位置为 pos,那么 a 只可能出现在 pos 的右边。

如果从结果考虑,那么第一种操作变成了删除一个集合中的数,第二种操作变成了加入一个没被删除过的最大值。目标状态为空。对于一个结果,其第二个操作的操作集合是已经固定的了。

从后往前枚举操作,假设目前 2 操作进行了 u 次,1 操作进行了 v 次。当前能删除的数字的方案数是 ba+vu。问题在于 2 操作中是否能找到一个最大值。

基于贪心的思想,我们贪心地删除当前最大的数。

定义 fi,j 表示填了 i 个数,第 i 个数填了 j 的方案数。倒序处理每一次操作。

  1. 如果这一次操作是 1cnt=0,那么现在有能够用来删除的数,而下一个用来删除的数在 fi+1,k(k<i)

  2. 如果这一次操作是 2 ,则判定是否有一个给你用的最大值。假设现在枚举的是 fi,jaijcnt+1 个数可用。而可以将可用数 0 的直接 ban 掉了。同时这个最大值可以抵消掉一个 1 操作,令 cntcnt+1

我们发现 i 是通用的所以不需要这一维状态。

所以 fi 表示现在填的最大值是 i 的方案数即可。

口胡完毕,代码待补。

update on 20230823 : 这是错误的。晚点补正解思路。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
const int modd = 998244353;
int a, b;
int c[10005], f[5005][5005];
int s, lim[10005], cnt, d = 1, k;
signed main(){
cin >> a >> b;
for(int i = 1; i <= a + b; i ++)
cin >> c[i];
k = a - b;
for(int i = a + b; i >= 1; i --){
if(c[i] == 1){
if(cnt) cnt --;
else k --;
}
else{
lim[k] = max(lim[k], d);
d ++, cnt ++;
}
}
for(int i = 0; i <= b; i ++)
f[a - b + 1][i] = 1;
for(int i = a - b; i >= 1; i --){
for(int j = lim[i]; j <= b; j ++){
if(j) f[i][j] = f[i][j - 1];
(f[i][j] += f[i + 1][j]) %= modd;
}
}
cout << f[1][b];
return 0;
}

[ARC027D] ぴょんぴょんトレーニング

n(1n300000) 个石柱,从第 i 个石柱可以跳到第 i+1,i+2,,i+hi(1hi10) 个石柱,有 d(1d5000) 组询问,每次问从 liri 有多少种走法。

note

0823 15:50

没法直接从线性 DP 的角度出发,那就直接走区间 DP 然后优化。

fi,j=k=1hifi+k,j

询问数量不多(?

只记录需要询问的可以吗。

fxi,xj=fxi,xj1×fxj1,xj

相邻的直接暴力线性 DP。设 d 为所有 hi 的最大值,时间复杂度应该是一个 O(q2d)

update on 0823 16:16:

然后会很惊喜的发现这是错的,因为询问用到的点不一定是必经点。

那怎么办呢?

我发现直接区间 dp 是可行的,滚动数组滚 10 维省个空间即可。 (1hi10) 我的神!

update on 0823 16:24:

不可行,因为直接区间 dp 是 n2 的。

那能考虑容斥吗?

似乎可行(?

反正 q 小。

update on 0824 21:02

虽然时间复杂度好像很像莫队的样子但是没想出怎么实现 add 和 del。

从头思考一遍。

反正看静态询问和这个数据范围就很莫队。

如果莫队的话,solve 函数是已经写完的,也就是只要 add 和 del 能实现一个这题就能直接切。

update on 0824 21:26

问了机房大佬 SError_,好像被他秒了(

大致思路是对每个点维护个转移矩阵然后每次乘一下就行了。

solution:

题意:有 n(1n300000) 个石柱,从第 i 个石柱可以跳到第 i+1,i+2,,i+hi(1hi10) 个石柱,有 d(1d5000) 组询问,每次问从 liri 有多少种走法。

考虑朴素的 DP。设状态 fi 代表从指定左端点走到 i 的方案数。只需要将每个 fi 贡献到所有 ji+hifj 即可。

初始值为指定的左端点的 f 值为 1,其它都为 0

那么有个 n2 的做法就是每次改变初始值然后 dp 一遍即可。

考虑优化这一 dp。我们发现 hi10,那么我们每次只需要记录 10f 值就够了。

假设现在记录了 fifi+9,现在要考虑第 i 个位置的转移,容易得知此时 fi 的值已经求出。

那么可以构造转移矩阵将 fifi+9 转移到 fi+1fi+10。矩阵的第一行有 hi1,而其他行则有第 i+1i 列为 1

这里以 hi6 为例给出转移矩阵,当hi 为其他情况则修改第一行即可。

[1111110000100000000001000000000010000000000100000000001000000000010000000000100000000001000000000010]

而根据 dp 的初始值,可构造初始矩阵如下。

[1000000000]

那么转移即变成了求初始矩阵与一段区间的乘积。不带修求区间乘就有很多很多种方法了。但是用线段树需要注意空间复杂度。

我喜欢暴力所以我了分块。

时间复杂度是 O(nn) 的,但是矩阵乘法有个 1000 的常数,非常大,理论上比较难过,只是这题的时限给的很宽能够跑过。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cmath>
#define int long long
using namespace std;
const int modd = 1000000007;
struct matr{
int st[10][10];
int n, m;
matr(int nl, int ml){
n = nl, m = ml;
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++)
st[i][j] = 0;
}
matr(int nl, int ml, bool fl){
n = nl, m = ml;
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++)
st[i][j] = (i == j);
}
matr(){
n = 10, m = 10;
}
int write(){
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++)
cout << st[i][j] << " ";
cout << "\n";
}
return 0;
}
}I(10, 10, 1);
matr A[300005], B[805];
matr mul(matr t1, matr t2){
matr m3(t1.n, t2.m);
for(int i = 0; i < t1.n; i ++)
for(int j = 0; j < t2.m; j ++)
for(int k = 0; k < t2.m; k ++)
(m3.st[i][j] += (1ll * (t1.st[i][k] % modd) * (t2.st[k][j] % modd)) % modd) %= modd;
return m3;
}
matr D(1, 10);
matr E(1, 10);
int n, q;
int T, bl;
int L[805], R[805], belong[300005];
int query(int l, int r){
if(belong[l] == belong[r]){
for(int i = l; i <= r; i ++)
E = mul(E, A[i]);
return E.st[0][0];
}
int u = belong[l], v = belong[r];
for(int i = l; i <= R[u]; i ++)
E = mul(E, A[i]);
for(int i = u + 1; i < v; i ++)
E = mul(E, B[i]);
for(int i = L[v]; i <= r; i ++)
E = mul(E, A[i]);
return E.st[0][0];
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
D.st[0][0] = 1;
for(int i = 1, x; i <= n; i ++){
cin >> x;
A[i].n = 10, A[i].m = 10;
for(int j = 0; j < x; j ++)
A[i].st[0][j] = 1;
for(int j = 0; j < 9; j ++)
A[i].st[j + 1][j] = 1;
}
T = 605, bl = n / T;
for(int i = 1; i <= bl; i ++)
B[i] = I, L[i] = R[i - 1] + 1, R[i] = L[i] + T - 1;
if(R[bl] < n) bl ++, L[bl] = R[bl - 1] + 1, R[bl] = n;
for(int i = 1; i <= bl; i ++)
for(int j = L[i]; j <= R[i]; j ++){
belong[j] = i;
B[i] = mul(B[i], A[j]);
}
cin >> q;
for(int i = 1, l, r; i <= q; i ++){
E = D;
cin >> l >> r;
if(l == r){
cout << 1 << "\n";
continue;
}
r --;
cout << query(l, r) << "\n";
}
return 0;
}

[ARC122E] Increasing LCMs

给定长度为 N 的正整数序列 {Ai},满足 Ai 单调升。

问是否能将 {Ai} 重排为序列 {xi},满足:

yi=LCM(x1,,xi)1i<N,yi<yi+1(即 yi 单调升)。

solution:

构造题。

很显然,任何序列的 lcm 都是单调不减的,而题目要求单调升,那么就不能有 lcm 相等的情况,即序列中的任意数都不能是前面的数的 lcm 的因数。从后往前考虑在序列中填什么数,假设目前未填写的数的集合为 S,目前尝试填的数是 x

有:

gcd(lcm{i[iSandix]},x)<x

用唯一分解定理转化一下得:

lcm{gcd(x,y[ySandyx])}<x

维护 S 集合,每次从 S 集合中暴力枚举出一个满足条件的 x,然后将 xS 中删除,重复操作直至 S 为空或找不到可以删除的数。当找不到可删除的数且 S 非空时无解。每次从 S 中删数可以看成减小问题规模的过程。

由于 lcm 在问题规模减小的过程中不会上升,所以越到后面能够删除的数会越来越多,所以只要找到一个可删除的数就能贡献到答案里而不必去考虑其它能删除的数。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
vector<int> ans;
bool vis[105];
int n;
int a[105];
int lcm(int x, int y){
return x / __gcd(x, y) * y;
}
int solve(){
int cnt = 0;
for(int i = 1; i <= n; i ++){
if(vis[i]) continue;
int nw = 1;
for(int j = 1; j <= n; j ++){
if(j == i || vis[j]) continue;
nw = lcm(nw, __gcd(a[i], a[j]));
}
if(nw < a[i]){
vis[i] = 1;
cnt = 1;
ans.push_back(a[i]);
break;
}
}
if(cnt) solve();
if(ans.size() == n) return 1;
return 0;
}
signed main(){
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i];
if(solve()){
cout << "YES\n";
reverse(ans.begin(), ans.end());
for(int i = 0; i < ans.size(); i ++)
cout << ans[i] << " ";
}
else cout << "NO\n";
return 0;
}

[AGC018C] Coins

x+y+z个人,第i个人有Ai个金币,Bi个银币,Ci个铜币。 选出x个人获得其金币,选出y个人获得其银币,选出z个人获得其铜币,在不重复选某个人的情况下,最大化获得的币的总数。 x+y+z<=1e5

solution:

做反悔堆时看到的题。

可以先钦定全选金币,令 BiBiAiCiCiAi 然后调整。

假设我们第 x 堆选择了铜币,第 y 堆选择了银币,考虑什么情况下交换两者决策(即让 x 选择银币,y 选择银币)不会让答案变差。显然需要:

Cx+ByBx+Cy

移项,得:

BxCxByCy

BiCi 排序然后用堆求出每个前缀的前 y 大和以及每个后缀的前 z 大和即可。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<queue>
#define int long long
using namespace std;
const int inf = 0xcfcfcfcfcfcfcfcf;
int x, y, z, n, ans;
struct node_t{
int a, b;
}c[100005];
bool cmp(node_t u, node_t v){
return u.a - u.b > v.a - v.b;
}
int f[100005], g[100005];
priority_queue<pair<int, int> > q;
signed main(){
cin >> x >> y >> z, n = x + y + z;
for(int i = 1, u, v, w; i <= n; i ++){
cin >> u >> v >> w;
ans += u, v -= u, w -= u;
c[i].a = v, c[i].b = w;
}
sort(c + 1, c + n + 1, cmp);
int nw = 0;
for(int i = 1; i <= n; i ++){
if(nw < y){
q.push(make_pair(-c[i].a, i));
nw ++;
f[i] = f[i - 1] + c[i].a;
}
else{
f[i] = f[i - 1] + c[i].a;
q.push(make_pair(-c[i].a, i));
int tp = q.top().second, vl = q.top().first;
q.pop();
f[i] += vl;
}
}
nw = 0;
while(!q.empty()) q.pop();
for(int i = n; i >= 1; i --){
if(nw < z){
q.push(make_pair(-c[i].b, i));
nw ++;
g[i] = g[i + 1] + c[i].b;
}
else{
g[i] = g[i + 1] + c[i].b;
q.push(make_pair(-c[i].b, i));
int tp = q.top().second, vl = q.top().first;
g[i] += vl;
q.pop();
}
}
int c1 = inf;
for(int i = y; i <= n - z; i ++)
c1 = max(c1, g[i + 1] + f[i]);
cout << ans + c1;
return 0;
}

[ARC059F] バイナリハック

小z得到了一个键盘,里面只有 1,0 和退格键。

0 可以打出一个 0 的字符串,键 1 同理。

退格键可以删除前面打出的那个字符。

小z可以操作这个键盘 N 次( N5000 ),求操作完成后打出来的字符串恰好是 S 的方案数。

注意:当前没有字符也可以使用退格键

note:

首先设 n=|S|。考虑一位一位填字符。可以将操作分为两步,第一步是在当前位置后任意的插入删除字符,然后再删到当前位置,第二步是插入当前字符,并将这一位之前的字符串锁定,之后无法删除。分步考虑:

考虑第一步操作。

先不考虑在字符串开头退格的情况。为了保证前面填好的字符不被删除,要随时保证在任意时刻插入的字符多于删除的字符。换种表示方式,如果将插入看成左括号,删除看成右括号,有这一段操作一定是合法的括号串。

插入有两种方案,设一个合法括号的左括号数量为 v,则这个合法括号串的贡献为 2v,而 v 显然为这个合法括号串的长度的一半。

因为这一步操作实际上对敲出最终字符串无贡献,因此这一步操作对答案的贡献仅与长度有关。对于一个给定长度 x,这个长度的每个合法括号串产生的贡献均为 2x2

因此只需要求给定长度的合法括号串数量。这是个卡特兰数问题。设长度为 x,则合法括号串数量为 Hx2,即长度为 x 的第一步操作序列的贡献为 2x2Hx2

再考虑在字符串开头退格的情况。我们需要得到的是经过 k 步第一位为空的方案数。设 gi,j 表示已经走了 i 步,长度为 j 的方案数。显然有:

gi,j={gi1,j+1+2gi1,j1 if j0gi1,j+1+gi1,j if j=0

边界为 g0,0=1

那么需要的经过 k 步第一位为空的方案数即为 gi,0

至此我们已经完成了求解对于任意 k,经过 k 步完成“在当前位置后任意的插入删除字符然后再删到当前位置”这一操作的方案数的统计。

继续考虑第二步操作

fi,j 表示经过 j 步后,考虑完前 i 位并把前 i 位变成最终串的样子的方案数,并且令这之后字符串的前 i 位锁定无法删除。

fi,j={fi1,k×2jk12Hjk12 if i1gj1,0 if i=1

边界是 fn+1,N+1

然后就得到了一个 O(n3) 的做法。

先记下 n3 代码再优化。

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
#include<string>
#define int long long
using namespace std;
const int modd = 1000000007;
int N, n;
string s;
int fac[10005], inv[10005], mi[10005], val[10005];
int ksm(int u, int v){
int ret = 1;
while(v){
if(v & 1) ret = ret * u % modd;
u = u * u % modd, v >>= 1;
}
return ret;
}
int C(int n, int m){
return (fac[n] * inv[n - m] % modd) * inv[m] % modd;
}
int H(int n){
return ((C(2 * n, n) - C(2 * n, n - 1)) % modd + modd) % modd;
}
int g[5005][5005], f[5005][5005];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> s, n = s.length();
n ++, N ++;
fac[0] = inv[0] = 1;
for(int i = 1; i <= N * 2; i ++)
fac[i] = fac[i - 1] * i % modd;
inv[N * 2] = ksm(fac[N * 2], modd - 2);
for(int i = (N * 2) - 1; i >= 1; i --)
inv[i] = inv[i + 1] * (i + 1) % modd;
mi[0] = 1;
for(int i = 1; i <= N * 2; i ++)
mi[i] = mi[i - 1] * 2 % modd;
for(int i = 1; i <= N * 2; i ++)
val[i] = H(i) * mi[i] % modd;
val[0] = 1;
g[0][0] = 1;
for(int i = 1; i <= N ; i ++)
for(int j = 0; j <= N; j ++)
if(j == 0) g[i][j] = (g[i - 1][j + 1] + g[i - 1][j]) % modd;
else g[i][j] = (g[i - 1][j + 1] + 2 * g[i - 1][j - 1] % modd) % modd;
for(int i = 1; i <= N; i ++)
f[1][i] = g[i - 1][0];
for(int i = 2; i <= n; i ++)
for(int j = 1; j <= N; j ++){
for(int k = 1; k <= j; k ++){
if((j - k - 1) < 0 || (j - k - 1) & 1)
continue;
(f[i][j] += (f[i - 1][k] * val[(j - k - 1) / 2]) % modd) %= modd;
}
}
cout << f[n][N];
return 0;
}

然后发现这个东西是个卷积,一看模数不是 NTT 模数,摆了(

solution:

被题解薄纱。

fi,j 表示完成了 i 步,匹配了 j 个字符的方案数。

显然有两种转移方式,一种是退格,一种是增加一个数字。

如果退格,那么会删除一个字符,并且不关心最后一个字符是否匹配。因为最后一个字符匹配或者不匹配的方案数是相同的,所以将 2fi1,j+1 贡献到 fi,j

如果增加字符,那么默认直接匹配,所以 fi,j=fi1,j1,注意这种情况需要 j0

然后做完了。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<string>
#define int long long
using namespace std;
const int modd = 1000000007;
int n;
string s;
int f[5005][5005];
signed main(){
cin >> n >> s;
f[0][0] = 1;
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= i; j ++)
f[i][j] = (f[i - 1][max(j - 1, 0ll)] + f[i - 1][j + 1] * 2 % modd) % modd;
cout << f[n][s.length()];
return 0;
}

代码很短。但是好像我的做法暴力卷积一下可以做到 O(nlog2n),比较优秀。所以算是想出了一个优秀正解的简化版(?

但是不是很会卷积,所以还是摆(


[ARC129D] -1+2-1

给定一个环a1,a2,,an(3n200000100ai100),其中an的后一个数为a1.

你可以执行任意次如下操作:

选择一个位置i(1in),将ai2,将与ai在环上相邻的两个数减1.

你需要将a数组中所有元素变为0.求最少操作次数,如果无解输出-1.

solution:

推式子题。

首先显然一定有

i=1nai=0

否则无解。

xi 为对 i 这个位置的操作次数。有:

2×xixi1xi+1+ai=0

di=xixi1

di+1di=ai

那么有 di=d1+j=1i1ai

i=1ndi=n×d1+i=1n(ni)×ai

又因为 i1ndi=0

所以有 d1=i=1n(ni)×ain

如果 d1 不是整数,即

i=1n(ni)×aimodn0

那么无解。

否则可以通过 d1 推出所有的 di

根据定义,xi=xi1+di,所以

xi=x1+j=2i0

需要构造方案使 x 最小,即让 x1 最小,扫一遍更新答案即可。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, s, s1, d1, x1;
int a[200005], sum[200005];
int d[200005];
signed main(){
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i], s += (n - i) * a[i], s1 += a[i], sum[i] = sum[i - 1] + a[i];
if(s % n != 0 || s1 != 0){
cout << -1;
return 0;
}
d1 = - s / n, s = 0;
for(int i = 1; i <= n; i ++)
d[i] = d1 + sum[i - 1];
for(int i = 2; i <= n; i ++){
s += d[i];
int xi = x1 + s;
if(xi < 0){
x1 += 0 - xi;
}
}
s = 0;
d[1] = x1;
for(int i = 1; i <= n; i ++)
d[i] += d[i - 1], s += d[i];
cout << s;
return 0;
}

[ABC180F] Unbranched

N 个点,M 条边且满足以下条件的图的数量:

  • 图中无自环

  • 每个点度数最多为 2

  • 所有连通块最多恰好有 L 个点

答案对 109+7 取模

2N300,1M,LN

solution:

图计数类 DP。

每个点度数最多为 2 这个条件简化一下得到图中每一个强连通分量要么是链要么是环。

套路设 fi,j 表示用了 i 个点,j条边,所有连通块不超过 L 个点的方案数。容斥下,答案为不超过 L 个点的方案数减去不超过 L1 个点的方案数。

钦定一个特殊点 1,使得 1 在用了 i1 个点的状态没有被使用,这样可以做到不重不漏。现在考虑计算 fi,j ,让 1 合并到前面的强连通分量中,分类讨论。

这个强连通分量是链

首先枚举强连通分量的大小 k,易得 k 满足 1kmin(i,j+1,L)。先在可以被挑选到链中的 n(ik)1 个点中挑选 k1 个点出来与 1 形成强连通分量,即贡献有一个 (n(ik)1k1)。然后对于一条链,有 k! 种排列方案,所以有 k! 的贡献。但是因为在一张图中,3  2  11  2  3 这两条链本质是相同的,所以在 k>1 的情况下需要将贡献除以 2

这个强连通分量是环

同理。k 满足 1kmin(i,j,L)。首先通过挑选点,贡献仍然有一个 (n(ik)1k1)。对于一个环,有 (k1)! 种排列方案。同理当 k>2 的情况需要将贡献除以 2

状态转移方程懒得写了,按照上面的推就行了。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int modd = 1000000007;
const int inv2 = 500000004;
int ksm(int u, int v){
int ret = 1;
while(v){
if(v & 1) ret = ret * u % modd;
u = u * u % modd, v >>= 1;
}
return ret;
}
int fac[305], inv[305];
int C(int u, int v){ return (fac[u] * inv[v] % modd) * inv[u - v] % modd; }
int f[305][305];
int L, n, m;
int solve(int l){
memset(f, 0, sizeof(f));
f[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= m; j ++){
for(int k = 1; k <= min(l, min(i, j + 1)); k ++){
(f[i][j] += f[i - k][j - k + 1] * C(n - i + k - 1, k - 1) % modd *
fac[k] % modd * (k > 1 ? inv2 : 1) % modd) %= modd;
}
for(int k = 2; k <= min(l, min(i, j)); k ++){
(f[i][j] += f[i - k][j - k] * C(n - i + k - 1, k - 1) % modd *
fac[k - 1] % modd * (k > 2 ? inv2 : 1) % modd) %= modd;
}
}
}
return f[n][m];
}
signed main(){
fac[0] = inv[0] = 1;
for(int i = 1; i <= 300; i ++)
fac[i] = i * fac[i - 1] % modd;
inv[300] = ksm(fac[300], modd - 2);
for(int i = 299; i >= 1; i --)
inv[i] = (i + 1) * inv[i + 1] % modd;
cin >> n >> m >> L;
cout << ((solve(L) - solve(L - 1)) % modd + modd) % modd;
return 0;
}

[ARC106F] Figures

N 个点,每个点有 ai 个孔,每次可以选择两个不同点,连接两个未被连接过的孔,有多少中方案使得最后形成一棵树。

solution:

Prufer 序列和生成函数什么的完全不会,所以记录个组合做法。

S=i=1ndi。显然有当 S<2n2 时无解。

考虑如何才能让整张图连成一棵树。考虑为每个点指定一个特殊的孔,那么每次连边操作可以看成从一个特殊孔连向一个非特殊孔。这里指定特殊孔的方案数显然是 i=1ndi

在树中,每个点仅有一个父亲,所以考虑为每个特殊点指定父亲。

需要指定父亲的特殊点仅有 n1 个,而且每个点都有可能成为根节点,不能重复经过点,因此最终方案是 (Sn)n2 次下降阶乘幂。

即有:

Ans=(i=1ndi)×(SN)n2

计算这个式子的时间复杂度是 O(n) 的。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
const int modd = 998244353;
int a[200005], n, sum, ans1 = 1, s, ans2 = 1;
signed main(){
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i], sum += a[i], (ans1 *= a[i]) %= modd;
s = sum - n;
s %= modd;
for(int i = 0; i < n - 2; i ++)
(ans2 *= (((s - i) % modd + modd) % modd)) %= modd;
cout << ans1 * ans2 % modd;
return 0;
}

[AGC016E] Poor Turkeys

N(2<=N<=400) 只火鸡, 编号为 1N , 有 M(1<=M<=105) 个人, 每人指定了两只火鸡 xy .
1.若 xy 都活着, 那么这个人将会等概率地随机吃掉一只
2.若 xy 恰好活着一只, 那么这个人将会吃掉活着的这只
3.若 xy 都已经死亡, 那么只好什么都不做
注意,第 1 个人到第 M 个人每个人依次行动
求有多少个 (i,j)(1<=i<j<=N) 满足在最终时刻第 i 只火鸡和第 j 只火鸡可能都还活着

solution:

SError_ 推荐的一道好玩题。

自己思考的时候大概想到了我们需要贪心的保护一只鸡的情况下求有多少鸡是一定活不下来的,但是没有想到倒流式 DP。

设状态 fi,j 表示如果要保护 i,那么在这之前需不需要把 j 炖了。初始值设为 fi,i=1

正着考虑比较难,考虑时光倒流。假设我们在某一步因为要保护某只鸡所以要炖了另一只,那么被炖的那只鸡一定要保证在这之前是安全的。因此状态 fi,j 也能理解为如果最后要留下 i,那么这之前是否要留下 j

探究下 i 一定会被吃的条件。假设有 fi,u=1fi,v=1,那么如果在时光倒流的过程中发现了一个操作 (u,v),那么 uv 就一定会有一个被删除,也就是说没法同时保护 uv,那推回去就是没法保护 i,即 i 一定会被吃。称这是结论一

Si 为如果要保护 i 那么需要炖了的鸡的集合。

考虑两只最终有可能留下来的鸡 uv。假设有两次操作 (u,x)(v,x),并且为了保护 uv,都分别必须要炖掉 x,那么必然有一只鸡没法拉到 x 挡枪。假设为了保护 uv 需要炖了 ij,那么会将 ij 保护起来当 uv 对待,因此归纳一下得到,如果 susv,那么必然有一步会出现矛盾,即没法同时保护 uv。称这是结论二

那么根据这两个结论直接 dp 然后模拟即可,时间复杂度为 O(nm+n3)

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int n, m;
int p1[100005], p2[100005];
bool f[405][405], flg[405];
int ans;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i ++)
cin >> p1[i] >> p2[i];
for(int i = 1; i <= n; i ++){
f[i][i] = 1;
for(int j = m; j >= 1; j --){
int u = p1[j], v = p2[j];
if(f[i][u] && f[i][v]){
flg[i] = 1;
break;
}
if(f[i][u])
f[i][v] = 1;
if(f[i][v])
f[i][u] = 1;
}
}
for(int i = 1; i <= n; i ++){
if(flg[i]) continue;
for(int j = i + 1; j <= n; j ++){
if(flg[j]) continue;
bool cg = 1;
for(int k = 1; k <= n; k ++)
cg = cg && (f[i][k] != 1 || f[j][k] != 1);
ans += cg;
}
}
cout << ans;
return 0;
}

[AGC015D] A or...or B Problem

lr 的整数中选择一个或多个,把这些整数按位或,求一共有多少种可能的结果。

1AB260

solution:

首先对于 lr 二进制下相同的高位可以直接去掉,因为对答案无贡献,即只用考虑二进制下 lr 的第一个不同位后面的部分。

z 为由相同的高位、二进制下第一个不同的位、一堆 0 组成的数,给出构造的例子。

l = 329 = 101 0 01001
r = 361 = 101 1 01001
z = 352 = 101 1 00000

那么可以把原区间拆成两部分,[l,z)[z,r]。考虑三种情况:仅在 [l,z) 中运算、仅在 [z,r] 中运算,同时在 [l,z)[z,r] 中运算。

仅在 [l,z) 中运算

显然,因为或运算不会进位,也不会让答案变小,一定可以构成 [l,z) 中的所有数。

仅在 [z,r] 中运算

找到 rz 的最高位的 1,将其后面都设为 1。设这个数为 p。即如下构造:

l = 329 = 101 0 0 1 001
r = 361 = 101 1 0 1 001
z = 352 = 101 1 0 0 000
p = 15 = 000 0 0 1 111

那么一定可以构造出 [z,p] 中的所有数。因为或运算不进位,所以仅能构造出 [z,p] 中的所有数。

同时在 [l,z)[z,r] 中运算

分析与上面雷同因此省略。

答案为区间 [l|z,z|(z1)]

将三个区间并起来即可得到最终答案。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int l, r, z, nd;
int getz(int u, int v){
int x = 1, z2 = 0;
while(u || v){
if(u % 2 != v % 2)
z2 = max(z2, x);
x <<= 1;
u >>= 1, v >>= 1;
}
int y = r;
int z3 = z2;
int nw = 1;
while(z3){
if((y & nw))
y -= nw;
if(z3 == 1)
y |= z2;
z3 >>= 1;
nw <<= 1;
}
nd = r - y;
return y;
}
signed main(){
cin >> l >> r;
if(l == r) {
cout << 1;
return 0;
}
z = getz(l, r);
int ans = z - l;
int l1 = z, r1 = z, l2 = (l | z), r2 = (z | (z - 1));
int kw = 0;
for(int i = 1; i <= nd; i <<= 1)
if(i & nd)
kw = i;
for(int i = 1; i <= kw; i <<= 1)
nd = nd | i;
int p = z + nd;
r1 = p;
if(r1 < l2)
ans += r1 - l1 + r2 - l2 + 2;
else
ans += r2 - l1 + 1;
cout << ans;
return 0;
}

[AGC021D] Reversed LCS

S 为字符串 S 反转后的字符串。定义一个字符串 S 的价值为 SS 的最长公共子序列的长度。

给一个字符串 S,可以修改 S 的最多 k 位,使得 S 的价值最大。

solution:

看起来是道水紫,开题到做完只用了 15min,然后一看 Difficulty 2284 乐了。

有一个结论,有 SS 的最长公共子序列的长度为 S 的最长回文子序列的长度,手推下即可得出,或者感性理解。

然后就直接区间 dp,做完了。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
using namespace std;
char s[305];int n, p;
int f[305][305][305];
int main(){
cin >> s + 1 >> p, s[0] = '#', n = strlen(s) - 1;
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= p; j ++)
f[i][i][j] = 1;
for(int r = 1; r <= n; r ++){
for(int l = r - 1; l >= 1; l --){
for(int k = 0; k <= p; k ++){
f[l][r][k] = f[l + 1][r][k], f[l][r][k] = max(f[l][r][k], f[l][r - 1][k]);
if(s[l] == s[r])
f[l][r][k] = max(f[l][r][k], f[l + 1][r - 1][k] + 2);
else if(k != 0)
f[l][r][k] = max(f[l][r][k], f[l + 1][r - 1][k - 1] + 2);
}
}
}
int ans = 0;
for(int i = 1; i <= n; i ++)
for(int j = i; j <= n; j ++)
ans = max(ans, f[i][j][p]);
cout << ans;
return 0;
}

[AGC055D] ABC Ultimatum

称一个长度为 3n,只由 A,B,C 组成的字符串是好的,当且仅当其能划分成 n 个长度为 3 的子序列,每个子序列都是 ABC,或者 BCA,或者 CAB。求把问号替换成 A,B,C 使得字符串是好的的方案数 mod 998244353

Solution

一道大 dp。

考虑转化条件。假设在目前的方案中前 i 个字母中 A,B,C 的个数分别有 XAiXBiXCi 个。

首先考虑字母 A 和字母 C 的关系。容易发现在 BCACAB 中, A 都紧贴在 C 的后面。那么 AC 前面的情况只有 ABC,因此能导出结论:

  • 对于 i[1,n],有 XCiXAi 的值一定不大于前缀 ABC 的个数。

同理,分析 BA 的关系, CB 的关系后,整理一下可以得到这样的一个结论:

  • A=maxXAXCB=maxXBXAC=maxXCXB,有 A+B+Cn,其中 max 代表前缀最大值。

必要性已经证明出来了,充分性自己不会证明,故扔张图上来。

记录下 ABC 的数量和 XAXBXC,然后 O(n6) 直接 dp 即可。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int modd = 998244353;
char s[50];
int n, m, ans;
int f[52][17][17][17][17][17];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> m, n = m * 3;
cin >> s + 1;
f[0][0][0][0][0][0] = 1;
for(int i = 0; i < n; i ++){
for(int a = 0; a <= m; a ++){
for(int b = 0; b <= m; b ++){
int c = i - a - b;
if(c < 0 || c > m) continue;
for(int xa = 0; xa <= m; xa ++){
for(int xb = 0; xb <= m; xb ++){
for(int xc = 0; xc <= m; xc ++){
if(xa + xb + xc > m) continue;
if(s[i + 1] == 'A' || s[i + 1] == '?')
(f[i + 1][a + 1][b][max(xa, a + 1 - c)][xb][xc]
+= f[i][a][b][xa][xb][xc]) %= modd;
if(s[i + 1] == 'B' || s[i + 1] == '?')
(f[i + 1][a][b + 1][xa][max(xb, b + 1 - a)][xc]
+= f[i][a][b][xa][xb][xc]) %= modd;
if(s[i + 1] == 'C' || s[i + 1] == '?')
(f[i + 1][a][b][xa][xb][max(xc, c + 1 - b)]
+= f[i][a][b][xa][xb][xc]) %= modd;
}
}
}
}
}
}
for(int i = 0; i <= m; i ++)
for(int j = 0; j <= m; j ++)
for(int k = 0; k <= m; k ++)
if(i + j + k <= m) (ans += f[n][m][m][i][j][k]) %= modd;
cout << ans;
return 0;
}

[ARC154D] A + B > C ?

有一个隐藏的长度为 n 的排列 P

你可以询问交互库 ? i j k,交互库会判断 Pi+Pj>Pk 是否为真命题,如果是则回答 Yes,否则回答 No。你需要在至多 25000 次询问内找出该排列。

交互库不自适应,即排列 P 是一开始就确定的。

1n2000

solution:

做了一段时间 DP 了做一道交互题玩一下(

首先对于 1,一定有询问 ? 1 1 xNo。假设 1 的位置为 p,那么从左到右滚一次就能找到 1 的位置,询问次数为 n1

有了 1 后我们可以利用询问 ? x p y 得到是否有 Px+1<Py,即 PxPy

那么有了比较就能排序了。用稳定的归并排序即可达到较少询问。调用 stl 中的 stable_sort 即可。

因为比较可能会有重复,所以记忆化一下可以再压缩一下询问次数。

code:

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
using namespace std;
int ans[2005], a[2005];
int p = 1, n;
int ask(int u, int v, int w){
cout << "? ";
cout << u << " " << v << " " << w << "\n";
fflush(stdout);
char s[4];
cin >> s;
return s[0] == 'Y';
}
int answer(){
cout << "! ";
for(int i = 1; i <= n; i ++)
cout << ans[i] << " ";
cout << "\n";
fflush(stdout);
return 0;
}
int rea[2005][2005];
bool cmp(int u, int v){
if(rea[u][v] != -1)
return rea[u][v];
rea[u][v] = (!ask(u, p, v));
return rea[u][v];
}
int main(){
cin >> n;
memset(rea, -1, sizeof(rea));
for(int i = 2; i <= n; i ++)
if(ask(p, p, i)) p = i;
for(int i = 1; i <= n; i ++)
a[i] = i;
stable_sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i ++)
ans[a[i]] = i;
answer();
return 0;
}

[AGC010D] Decrementing

黑板上写着 N 个整数。第 i 个整数是 Ai ,它们的最大公约数为 1

高桥君和青木君将使用这些数来玩一个游戏。高桥君在这个游戏中是先手,他们将轮流进行以下操作(以下两步相当于一次操作):

  • 选择黑板中大于 1 的一个数,将其减 1
  • 此后,将黑板上所有数全部除以所有数的最大公约数。

当黑板上的数全部为 1 时,不能再进行操作的人就失败了。两人都选择最好的方式行动,请求出哪边会最终胜利。

  • 1N105
  • 1Ai109
  • A1AN 的所有数的最大公约数为 1

solution

好玩博弈。

考虑什么情况是先手必胜,很容易得到是像这样的东西:

kkk+1kk

这个时候只要把 k+1 操作一下,后手就无法操作了。

考虑回溯一步,因为最优情况一定会避免如上状态,因此上一步后手一定会让其中一个 k 减去 1(如果可以这么操作)。

因此得到 k=1

当有 p 满足 ap=1 时,其实胜负已经确定了。可以得到,当此时场上的偶数个数为奇数时,先手胜,否则后手胜。

扩展一下,场上的偶数个数为奇数时,先手一定能够保护好这奇数个偶数,直到场上出现 1,此时先手必胜。

那么如果场上有偶数个偶数时,先手就会很危险,急切需要改变场上的数字的奇偶性。

为了改变奇偶性,唯一的方法是用一步做到 2|gcdai 刷新 a 数组的奇偶性情况,否则后手就可以保护住后手面对的奇数个偶数的情况。

即在场上没有 1 的情况,如果只剩下一个奇数,那么先手只能操作这个数,然后数列除以 gcd,奇偶性重置,将先手权交给后手,否则先手必败。

至此已经判断完所有情况,依次执行以下流程即可判断:

  1. 判断是否出现了 1,如果出现了 1 则直接判断即可。

  2. 记场上的偶数的个数为 t,如果 t 是奇数则先手必胜。

  3. 此时若场上奇数的个数大于 1,则后手必胜。

  4. 否则操作场上唯一的奇数,然后全部除以数列的 gcd,先手权转交后手后回到流程开始继续判断。

设数列值域为 W。因为只有操作 4 会继续判断,且每次值域一定会减小一半,因此这个流程是 O(nlogW) 的,然后算 gcd 带一支 log,因此总时间复杂度 O(nlog2W)

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int n, a[100005];
int checkone(){
int t = 0;
for(int i = 1; i <= n; i ++)
if(!(a[i] & 1)) t ++;
return (!(t & 1)) + 1;
}
int check(){
int t = 0, t1 = 0, p;
for(int i = 1; i <= n; i ++)
if(a[i] == 1) return checkone();
else if(!(a[i] & 1)) t ++;
else t1 ++, p = i;
if(t & 1) return 1;
if(t1 > 1) return 2;
a[p] --;
int gcd = a[1];
for(int i = 2; i <= n; i ++)
gcd = __gcd(a[i], gcd);
for(int i = 1; i <= n; i ++)
a[i] /= gcd;
return 3;
}
signed main(){
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i];
int q, opt = 1;
while((q = check()) == 3) opt ^= 1;
if(opt == (q & 1)) cout << "First";
else cout << "Second";
return 0;
}

[AGC005D] ~K Perm Counting

如果一个排列 P 满足对于所有的 i 都有 |Pii|k,则称排列 P 为合法的。现给出 nk,求有多少种合法的排列。

由于答案很大,请输出答案对 924844033 取模的结果。

【数据范围】

2n2×1031kn1

note

这题做的比较久,所以少见的记一下 note。

看到这题想到是个图论题。

首先用图论的角度思考了一下错排问题,将 Pi=j 表示为 Pij 连边,问题变成了由若干个环、无自环组成的有向图的个数,图论计数 DP 可以解决。

或者说用容斥的角度思考了一下错排问题,这种时候就是一个简单 DP。

那么考虑原题的图论意义。

考虑 iPi 连边,那么原条件就会变成有一堆边不能连。

那么按一道构造题的思路,把对 k 的同余类拍成一个表格,像下面这样( n=12k=3

147102581136912

然后就是不能连横向边,但是到处跑来跑去连环很烦,所以止步于此/ll

solution

那如果把 Pii 看作两个点呢?

这样就变成了一张二分图,左部是表示 Pi 的点,右部是表示 i 的点,那么问题就变成了在这个二分图上连上 n 条边补成一张完全图,而有些不能连的边。

一样的把不能连的边拍成一张表格。

1P47P10P14P7102P58P11P25P8113P69P12P36P912

因为这样是一次一次连边而不是连一堆环所以好做多了。

那就和上面的容斥思路一致,改为考虑连这些不可连的边。注意到每个点入度出度为 1 所以一个点不能同时连接左右两个点。

在表格上 DP 可以,但没必要所以我们可以把表格拍到一条线上,然后记录一下每一行的末尾。
fi,j,0 表示考虑了表格的前 i 个数,连了 j 条不可连的边,(i,i1) 连/不连的方案数。

显然有状态转移方程:

fi,j,0=fi1,j,0+fi1,j,1

fi,j,1=fi1,j1,0

注意在表格中每一行开头无法用第二条柿子转移。

然后套路容斥即可。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
const int modd = 924844033;
int n, ans, k;
int f[4005][4005][2];
int fac[2005];
bool vis[4005];
signed main(){
cin >> n >> k;
fac[0] = 1;
for(int i = 1; i <= n; i ++)
fac[i] = fac[i - 1] * i % modd;
for(int i = 1, nw = 0; i <= k; i ++){
for(int u = 0; u <= 1; u ++){
for(int j = i; j <= n; j += k){
nw ++;
if(i != j) vis[nw] = 1;
}
}
}
f[0][0][0] = 1;
for(int i = 1; i <= 2 * n; i ++){
for(int j = 0; j <= n; j ++){
f[i][j][0] = (f[i - 1][j][0] + f[i - 1][j][1]) % modd;
if(vis[i] && j)
f[i][j][1] = f[i - 1][j - 1][0];
}
}
for(int i = 0; i <= n; i ++){
if(i & 1) ans = (ans - (((f[n * 2][i][0] + f[n * 2][i][1]) % modd) * fac[n - i]) % modd + modd) % modd;
else ans = (ans + (((f[n * 2][i][0] + f[n * 2][i][1]) % modd) * fac[n - i]) % modd) % modd;
}
cout << ans;
return 0;
}

[AGC030D] Inversion Sum & [CF258D] Little Elephant and Broken Sorting

([AGC030D] Inversion Sum)

给你一个长度为 n 的数列,然后给你 q 个交换或不交换操作,你可以选择操作或者不操作,问所有情况下逆序对的总和。

答案需要对 109+7 取模。

n3000q3000

([CF258D] Little Elephant and Broken Sorting)

有一个1n的排列,会进行m次操作,操作为交换a,b。每次操作都有50%的概率进行。

求进行m次操作以后的期望逆序对个数。

n,m1000

solution

Atcoder 你怎么抄 CF 啊(

[CF258D] Little Elephant and Broken Sorting

首先 和的期望 等于 期望的和,一个逆序对会产生 1 的贡献,即问题转化为求对于任意两个位置 i,j(i<j)Ai>Aj 的概率和。

然后是一个概率 dp。设 fi,j 表示 Ai>Aj 的概率,初始值为 fi,j=[Ai>Aj]

对于一次操作 (u,v)(uv)。首先,因为这是个排列所以有 AuAv,所以有 fu,v+fv,u=1,交换完后有 fu,v=fv,u=0.5

考虑第三个点 x(xu,v),因为有 12 的概率交换 u,v,所以会均摊 Ai>AuAi>Av 的概率,同理也会均摊 Ai<AuAi<Av 的概率。这样就完成了转移。

即对于一次操作状态转移方程为:

fu,v=fv,u=0.5

fu,i=fv,i=(fu,i+fv,i)2

fi,u=fi,v=(fi,u+fi,v)2

然后就做完了。

[AGC030D] Inversion Sum

如果做过上面那道题,这题很显然,只需要在上一题的基础上乘上 2m 即可,做完了。

是这样,吗?

注意到这题不是排列,因此没有 AuAv,也就是每次操作不会让 fu,v=fv,u=0.5

那也很显然,将概率均摊一下即可。

fu,v=fv,u=fu,v+fv,u2

然后做完了。但是其实如果没做过上面那题,这题最有意思的地方也就是把 总和 转换为 期望乘上方案数,也是一个比较难想到的点。

code

[CF258D] Little Elephant and Broken Sorting

#include<iostream>
#include<fstream>
#include<algorithm>
#include<iomanip>
#define int long long
using namespace std;
int n, T;
double a[5005];
double f[5005][5005], ans;
signed main(){
cin >> n >> T;
for(int i = 1; i <= n; i ++)
cin >> a[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
f[i][j] = (double)(a[i] > a[j]);
while(T --){
int u, v;
cin >> u >> v;
for(int i = 1; i <= n; i ++){
if(i == u || i == v) continue;
f[i][u] = f[i][v] = (f[i][u] + f[i][v]) / 2.0;
f[u][i] = f[v][i] = (f[u][i] + f[v][i]) / 2.0;
}
f[u][v] = f[v][u] = 0.5;
}
for(int i = 1; i <= n; i ++)
for(int j = i + 1; j <= n; j ++)
ans += f[i][j];
cout << fixed << setprecision(9) << ans;
return 0;
}

[AGC030D] Inversion Sum

#include<iostream>
#include<fstream>
#include<algorithm>
#include<iomanip>
#define int long long
using namespace std;
int n, T;
int a[5005];
int f[5005][5005], ans;
const int modd = 1000000007, inv2 = 500000004;
int ret = 1;
signed main(){
cin >> n >> T;
for(int i = 1; i <= T; i ++)
(ret *= 2) %= modd;
for(int i = 1; i <= n; i ++)
cin >> a[i];
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
f[i][j] = (a[i] > a[j]);
while(T --){
int u, v;
cin >> u >> v;
f[u][v] = f[v][u] = ((f[u][v] + f[v][u]) % modd * inv2) % modd;
for(int i = 1; i <= n; i ++){
if(i == u || i == v) continue;
f[i][u] = f[i][v] = ((f[i][u] + f[i][v]) % modd) * inv2 % modd;
f[u][i] = f[v][i] = ((f[u][i] + f[v][i]) % modd) * inv2 % modd;
}
}
for(int i = 1; i <= n; i ++)
for(int j = i + 1; j <= n; j ++)
(ans += f[i][j]) %= modd;
cout << ans * ret % modd;
return 0;
}

[AGC016D] XOR Replace

一个序列,一次操作可以将某个位置变成整个序列的异或和。 问最少几步到达目标序列。

solution

翻 SError_ 的做题记录翻到的。

懒得写了明天补。

还是懒得写后天补……

一周后终于开始补了!

重要性质:设 x 为整个序列的异或和,假设我们用 x 去替换一个 Ai,那么我们发现 x 就变成了 Ai

那么我们把 x 放到 An+1 的位置,同时也把 B 数组的异或和放到 Bn+1 的位置,然后我们可把操作认为是每次交换 A 数组的两个数,问至少要多少次操作可以把 A 数组变得和 B 数组一样。

那么就可以先把无解判掉了。有解当且仅当此时 AB 数组完全一致。

感觉有点像小时候玩的华容道啊,那就按照华容道的玩法,假设第 n+1 个位置是华容道的空位,那么我们可以用 n+1 替换 x1,然后用 x1 替换 x2,用 x2 替换 x3 这么持续下去,直到回到 n+1,形成一个循环。

这启发我们可以建图。首先 Ai=Bi 的时候不用处理,否则将 AiBi 连边,会形成若干个连通块,然后每个连通块内,对于每个 AiBi,可以一步达成目的,而对于不同的连通块,还需要一步把 n+1 替换过去。

也就是,最终答案 = 总边数 + 联通块数 1

是这样吗?

如果第 n+1 个点是个孤立点,那么最后还是需要把 n+1 挪回去,所以不用 1

可以用并查集维护连通块,然后就没了。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int n, sm, sm1, cnt;
int a[100005], b[100005];
int c[100005], d[100005];
int fa[500005], ct[500005], num;
int find(int u){if(u == fa[u]) return u; return fa[u] = find(fa[u]);}
signed main(){
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i], sm ^= a[i], c[i] = a[i];
for(int i = 1; i <= n; i ++)
cin >> b[i], sm1 ^= b[i], d[i] = b[i];
n ++, a[n] = sm, b[n] = sm1, c[n] = sm, d[n] = sm1;
sort(c + 1, c + n + 1), sort(d + 1, d + n + 1);
for(int i = 1; i <= n; i ++)
if(c[i] != d[i]){cout << -1;return 0;}
for(int i = 1; i <= n; i ++)
if(a[i] != b[i] || i == n)
ct[++ num] = a[i], ct[++ num] = b[i], cnt += (i < n);
if(!cnt){cout << 0; return 0;}
sort(ct + 1, ct + num + 1);
num = unique(ct + 1, ct + num + 1) - ct - 1;
for(int i = 1; i <= num; i ++)
fa[i] = i;
for(int i = 1; i <= n; i ++){
if(a[i] != b[i] || i == n){
a[i] = lower_bound(ct + 1, ct + num + 1, a[i]) - ct;
b[i] = lower_bound(ct + 1, ct + num + 1, b[i]) - ct;
fa[find(a[i])] = find(b[i]);
}
}
for(int i = 1; i <= num; i ++)
if(fa[i] == i) cnt ++;
cout << cnt - 1;
return 0;
}

[ARC124D] Yet Another Sorting Problem

给定长度为 n+m 的排列 p,其中 1n 位置为白色,n+1n+m 位置为黑色,每次操作定义为交换一个白色位置与一个黑色位置的数,求把 p 变成升序的最少操作次数。

n,m105

第一行输入 n,m,第二行输入 pi,保证其是 n+m 的排列;输出一行一个整数,代表最少操作数。

solution

做完上面那题后做这题轻松很多。

虽然还是看了题解(

建图,形成若干个环。画一下发现,当我们对一条边 (u,au) 的两个端点进行一次交换时,就会让 au 脱离这个环,也就是对于一个环,如果它的点数是 x,我们就能用 x1 步把环上的每一个数脱离。

但是前提是这个环需要可以交换,也就是这个环不能是同色的。

我们肯定想让环的数量越多越好,所以我们直接将异色的同色环两两匹配,即一次交换把这两个环拼起来,然后把不能匹配的随便找个异色环塞进去。假设有异色环 c 个有白色环 x 个,黑色环 y 个,则这一步消耗步数 max(x,y),同时让 c 增加 c+min(x,y)

因为每个点都属于一个环,而每个环会让答案减去 1,所以答案还要加上 n+mc

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int n, m;
int a[200005];
bool vis[200005];
int cnt, cnt1, cnt2, cnt3, ans;
signed main(){
cin >> n >> m;
for(int i = 1; i <= n + m; i ++)
cin >> a[i];
for(int i = 1, u, flg = 0, flg1 = 0; i <= n + m; i ++){
flg = 0, flg1 = 0;
if(vis[i]) continue;
vis[i] = true, u = i;
if(a[u] == u){cnt3 ++;continue;}
if(u <= n) flg |= 1;
else flg1 |= 1;
while(a[u] != i){
u = a[u], vis[u] = true;
if(u <= n) flg |= 1;
else flg1 |= 1;
}
cnt ++;
if(flg && !flg1) cnt1 ++;
else if(!flg && flg1) cnt2 ++;
}
ans = max(cnt1, cnt2);
cnt -= max(cnt1, cnt2);
ans += (n + m - cnt3) - cnt;
cout << ans;
return 0;
}

[AGC027D] Modulo Matrix

  • 构造一个 NN 的矩阵. 要求:
    • 所有元素互不相同.
    • 满足 ai,j1015.
    • 对于任意两个相邻的数字 ,max(x,y)modmin(x,y) 都相等,且均为正整数。
  • 可以证明方案一定存在.

solution

可以想到黑白染色,然后让白点的权值为周围黑点的 lcm+1 即可符合题意。

但是如果黑点随便填,那么随便填下就超过 1015 了。

所以要尽量使白点周围的黑点的共同因子尽可能大。为了匹配各种黑点,所以考虑从对角线入手,对每条主对角线和副对角线分配两个质数,这样每两个黑点做 lcm 运算时就会通过除以 gcd 吃掉一个质数。

因为要为 2n1000 个数分配质数,而第 1000 个质数大概在 104 级别,所以 lcm1016 级别,还是无法通过。

那么调整下质数的顺序,相邻的对角线按大-小-大-小这么分配质数即可。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int prime[2005], tot;
bool vis[100005];
int n;
int a[505][505];
int l[505], r[505];
int prework(){
for(int i = 2; i <= 8000; i ++){
if(!vis[i]) prime[++ tot] = i;
if(tot == 1000) break;
for(int j = 1; j <= tot && i * prime[j] <= 8000; j ++){
vis[i * prime[j]] = 1;
if(i % prime[j] == 0) break;
}
}
return 0;
}
int gcd(int u, int v){
if(u < 0 || v < 0){
cout << "< 0!!!\n";
exit(0);
return 0;
}
if(u < v) return gcd(v, u);
if(v == 0) return u;
return gcd(u % v, v);
}
int lcm(int u, int v){
// cout << u << " " << v << "\n";
// cout << u << " " << v << "\n ----------- \n";
return u / gcd(u, v) * v;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
prework();
for(int i = 2; i <= 500; i += 2)
swap(prime[i], prime[1000 - i]);
cin >> n;
if(n == 2){
cout << "4 7\n23 10";
return 0;
}
for(int i = 1; i <= n; i ++)
l[i] = prime[i], r[i] = prime[i + n];
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if((i & 1) == (j & 1)){
int u = (i - j) / 2 + ((n + 1) / 2);
int v = (i + j) / 2;
a[i][j] = l[u] * r[v];
}
}
}
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if(!a[i][j]){
int lm = 1;
if(i + 1 <= n) lm = lcm(lm, a[i + 1][j]);
if(i - 1 >= 1) lm = lcm(lm, a[i - 1][j]);
if(j + 1 <= n) lm = lcm(lm, a[i][j + 1]);
if(j - 1 >= 1) lm = lcm(lm, a[i][j - 1]);
a[i][j] = lm + 1;
}
cout << a[i][j] << " ";
if(a[i][j] > 1e15){
cout << "WA!";
exit(0);
}
}
cout << "\n";
}
return 0;
}

[ARC053D] 2 つの山札

给定两个1n的排列a1N,b1N,你需要执行以下操作2N2次以生成一个长度为2N2的序列

选择a,b之一(被选的序列长度必须2),将它的第一个数字删除,将另一个序列的第一个数字添加到要生成的序列的末尾

问最后能得到多少种不同的序列

2N1000a,b1N的排列

solution

题意看起来非常复杂,而这题的一难点在于转化题意。

画一个矩阵,横排代表 a 排列,纵排代表 b 排列,每个格子代表一个状态,格子 (i,j) 代表现在 a 排列的第一个数字是 aib 排列的第一个数字是 bi

问题转化为从左上角走到右下角,只能向右走或向下走来转移状态,形成的不同数列的数量。

先不考虑重复,可以得到个很简单的方程:fi,j=fi1,j+fi,j1

然后来去重。

我们发现出现重复的条件是走到格子 (i,j) 满足 ai=bj

此时去找在这之前也满足 ai=bj(ik,jk),将它们提出来再放在一个表格里,我们发现问题变为求从这一个表格的左上角走到右下角,只能往右或往下走,不能经过直线 y=x 的方案数,这是个卡特兰数的问题。

推导下得到方程,其中 C 代表卡特兰数:

f[i][j]=f[i][j1]+f[i1][j][ai=bj]k1[aik=bjk]f[ik][jk]C(t1)

因为满足 ai=aj 的点 (i,j) 的数量只有 n 个,所以时间复杂度是 O(n2) 的。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
int a[1005], b[1005];
int C[1005];
int f[1005][1005];
int n;
const int modd = 1000000007;
int jc[2005], inv[2005];
int ksm(int u, int v){
int ret = 1;
while(v){
if(v & 1) (ret *= u) %= modd;
v >>= 1, (u *= u) %= modd;
}
return ret;
}
int getC(int u, int v){
return (jc[u] * inv[v] % modd) * inv[u - v] % modd;
}
signed main(){
C[0] = 1;
jc[0] = inv[0] = 1;
for(int i = 1; i <= 2000; i ++)
jc[i] = jc[i - 1] * i % modd;
inv[2000] = ksm(jc[2000], modd - 2);
for(int i = 1999; i >= 1; i --)
inv[i] = inv[i + 1] * (i + 1) % modd;
for(int i = 1; i <= 1000; i ++)
C[i] = (getC(2 * i, i) - getC(2 * i, i - 1) + modd) % modd;
cin >> n;
for(int i = 1; i <= n; i ++)
cin >> a[i];
for(int i = 1; i <= n; i ++)
cin >> b[i];
f[1][1] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if(i > 1 || j > 1) f[i][j] = (f[i - 1][j] + f[i][j - 1]) % modd;
if(a[i] == b[j]){
for(int k = 1, cnt = 0; k < i && k < j; k ++){
if(a[i - k] == b[j - k])
f[i][j] = ((f[i][j] - C[cnt] * f[i - k][j - k] % modd) + modd) % modd, cnt ++;
}
}
}
}
cout << f[n][n];
return 0;
}

[AGC014D] Black and White Tree

  • 给出一颗 N 个节点组成的树,每个节点都可以被染成白色或者黑色;

  • 有高桥(先手)和青木(后手)两个人————高桥可以把任意一个点染成白色,青木则可以把任意一个点染成黑色,每个点只可染色一次。

  • 重复上述操作直到所有点都被染色后,只执行一次执行以下操作:

    1. 把所有青木染成黑色的节点的相邻的白点感染成“次黑色”。

    2. 次黑色不能继续感染白点。

  • 若操作完毕后仍还有白点存在,即高桥(先手)胜,反之则青木(后手)胜。

  • 现在给出这棵树,问当前此树是先手必胜还是后手必胜。

solution

如果一个黑色节点和一个白色节点相邻,那么我们说黑色节点控制了白色节点。

那么后手的胜利条件即为所有白色节点都被控制。现在考虑后手的策略。

首先如果这棵树有完美匹配(即用一半的边覆盖整棵树),那么显然后手可以在先手染白一个点后直接染黑其匹配点,这样所有白点都会被控制。

因此只考虑这棵树没有完美匹配的情况。

考虑一个叶子节点。如果叶子节点的父亲节点被染成了白色,那么这个叶子节点一定要染成黑色,否则叶子节点将永远无法被控制。

现在假设进行了一次如上操作,两个点 u, v 被分别染成了黑色和白色。因为白点已经被控制住了,并且黑点无法再控制其他节点,所以这两个节点对游戏已经没有任何影响了,可以删去。

这样持续进行下去,每次删除两个点。因为这棵树没有完美匹配,所以最后肯定剩下很多零散的点。

那么这个时候先手随便取一个零散的点,后手找不到任意一个点去控制住这个点,因此先手胜。

因此我们得到结论:后手必胜的充要条件是这棵树有完美匹配

接下来考虑如何判定一棵树是否有完美匹配。

首先如果这棵树的点数是奇数,那么显然没有。

我们尝试递归构造出完美匹配。

在一个完美匹配中,任意一棵子树只有两种状态,要么这棵子树本身是完美匹配,要么这棵子树删去根节点后是完美匹配,并且根节点与父亲节点连边形成匹配。边界为,空树是完美匹配,而叶子节点则需要和父亲节点匹配。

假设现在考虑到一个点 u,有如下情况:

  1. 如果 u 的子节点中有两棵子树状态为需要和父亲节点匹配来形成完美匹配,那么这无法做到,因此这棵树没有完美匹配。

  2. 如果 u 的子结点中有一棵子树需要和父亲节点连边,那么将 u 和这一子树匹配。其他子树已经是完美匹配了,因此此时这棵子树形成完美匹配。

  3. 如果 u 的子节点全部都是完美匹配,那么 u 需要和父亲节点匹配来形成完美匹配。

按这种条件已经可以进行 dp 判定了。但是还能够再简洁些。

我们考虑每个子树的大小。容易发现需要和父亲节点匹配的子树大小一定是奇数,而本身形成完美匹配的子树大小一定是偶数。那么这三个条件等价于判断子节点的子树大小的奇偶性,如果一个节点的子结点中有两棵子树的大小为奇数则不可行。

至此完成了完美匹配的判定,根据完美匹配的判定判断先手必胜或必败即可。

code

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
//#define int long long
using namespace std;
int n;
vector<int> edge[100005];
int siz[100005];
bool flg = true;
int dfs(int u, int fa) {
siz[u] = 1;
bool chk = 0;
for (int v : edge[u]) {
if (v == fa) continue;
dfs(v, u);
if (chk == 1 && (siz[v] & 1))
flg = false;
chk = chk || (siz[v] & 1), siz[u] += siz[v];
}
return 0;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
if (n & 1) {
cout << "First";
return 0;
}
for (int i = 1, u, v; i < n; i ++)
cin >> u >> v, edge[u].push_back(v), edge[v].push_back(u);
dfs(1, 0);
if (flg == false) cout << "First";
else cout << "Second";
return 0;
}

[AGC008D] K-th K

给你一个长度为 N 的整数序列 X,请判断是否存在一个满足下列条件的整数序列 a,如果存在,请构造一种方案。

条件如下:

  1. a 的长度为 N2,并且满足数字 1,2,,N 都各出现恰好 N 次。

  2. 对于 1iN,数字 ia 中第 i 次出现的位置是 Xi

solution

其实数字 ia 中第 i 次出现的位置是 Xi 代表着三个条件,即:

  1. xi 位置是 i
  2. xi 位置前面有 i1i
  3. xi 位置后面有 nii

考虑一个一个条件去满足。先在 xi 填上 i 把第一种数填好。

然后贪心的,按 xi 从小到大排序,然后把答案一个一个填到较小的位置,处理第二种情况。

同理,按 xi 从大到小排序后处理第三种情况即可。

没什么好讲的,除了能够拆成三个条件做以外没什么了。

code

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
//#define int long long
using namespace std;
pair<int, int> a[505];
int n;
int lst, ans[250005];
bool cmp(pair<int, int> u, pair<int, int> v) {
return u.first > v.first;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++)
cin >> a[i].first, a[i].second = i, ans[a[i].first] = i;
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i ++) {
int cnt = a[i].second - 1;
if (cnt != 0)
for (int j = 1; j < a[i].first; j ++) {
if (!ans[j]) ans[j] = a[i].second, cnt --;
if (cnt == 0) break;
}
if (cnt) {
cout << "No";
return 0;
}
}
for (int i = n; i >= 1; i --) {
int cnt = n - a[i].second;
if (cnt != 0)
for (int j = n * n; j > a[i].first; j --) {
if (!ans[j]) ans[j] = a[i].second, cnt --;
if (cnt == 0) break;
}
if (cnt) {
cout << "No";
return 0;
}
}
cout << "Yes\n";
for (int i = 1; i <= n * n; i ++)
cout << ans[i] << " ";
return 0;
}

[AGC032D] Rotation Sort

给定一个排列, 你可以花费A使一个区间最左边的数跑到最右边, 或者花费B的代价使最右边到最左边, 求把整个序列变成升序的最少花费.

solution

首先任意一个数要么不操作,要么只操作一次。因为如果操作两次,那么完全可以在第一次操作的时候跑到第二次操作跑到的地方。

显然,那些没有动过的数会形成一个上升序列。

考虑 dp。设 fi,j 表示现在考虑了前 i 个数,上一个没有动过的数是 j 的方案数。

主动型转移。如果下一个数决定不动,或是向右移动,那么条件是下一个数需要大于前一个不动的数。即有转移:

fi,jfi+1,i+1(aj<ai+1)

fi,jfi+1,j+A(aj<ai+1)

否则下一个数一定只能向左移动,去跨过上一个不动的数达成上升。

fi,jfi+1,j+B(aj>ai+1)

然后做完了。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int f[5005][5005];
int n, a[5005], A, B;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> A >> B;
for(int i = 1; i <= n; i ++)
cin >> a[i];
memset(f, 0x3f, sizeof(f));
f[0][0] = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j <= i; j ++){
if(a[i + 1] > a[j])
f[i + 1][i + 1] = min(f[i][j], f[i + 1][i + 1]), f[i + 1][j] = min(f[i][j] + A, f[i + 1][j]);
else f[i + 1][j] = min(f[i + 1][j], f[i][j] + B);
}
}
int ans = inf;
for(int i = 0; i <= n; i ++)
ans = min(ans, f[n][i]);
cout << ans;
return 0;
}

[AGC023C] Painting Machines

  • 有一排 n 个格子,从左到右编号为 1n
  • n1 个机器,从左到右编号为 1n1,操作第 i 个机器可以将第 i 个和第 i+1 个格子染黑。
  • 定义一个 n1 的排列 P 的分数为,依次操作 P1,P2,,Pn1,第一次染黑所有格子的时刻。
  • 求所有排列 P 的分数之和,对 109+7 取模。
  • 1n106.

Solution

考虑操作第 i 个机器。如果在操作第 i 个机器的时候,第 i 个格子和第 i+1 个格子都已经是黑色的了,那么这次操作就是无效的。并且因为第 i 个格子和第 i+1 个格子被染黑了,所以在这之前已经操作了第 i1 个机器和第 i+1 个机器。

即,如果对于排列中的一个数 Pi,如果有 Pi1Pi+1 出现在 i 前,那么 Pi 就是无效操作。

那么对于一个排列 P,假设在 t 时刻染黑了所有格子,那么意味着第 t 次操作是有效操作,而第 t+1 到第 n1 次操作一定都是无效的。将第 1t 这一序列称为 “有效序列”,第 t+1 到第 n 序列称为 “无效序列”。

考虑通过枚举无效序列的长度来统计方案,设此时无效序列的长度为 i,此时 t 是定值 ni1,因此我们只需要算有多少种排列的无效序列长度为 i。设 fi 为无效序列长度大于或等于 i 的方案数。

首先,对第一台机器的操作和对第 n1 台机器的操作总是有效的,因为没有办法在不操作这两台机器的情况下染黑第 1 个格子和第 n 个格子。

可以得到一个结论,如果有 x 在无效序列中,那么 x+1x1 都在有效序列中。这个结论可以反证法证明。假设 x1x 都在无效序列中,那么对于 x 而言,需要 x1x 前面,而对于 x1 而言,又需要 xx1 前面,可以推出矛盾,x+1 同理。

我们在原序列中选 i 个数出来组成无效序列,不能选第 1 和第 n1 个数,并且不能选择相邻的数。

通过组合数可以得到选数的方案数为 (ni2i)

那么有: fi=(ni2i)×(ni1)!×i!

那么无效序列长度恰好为 i 的方案数即为 fifi+1

最终答案为 i=1nfi×(ni1)

code

#include<iostream>
#include<fstream>
#include<algorithm>
#define int long long
using namespace std;
const int modd = 1000000007;
int n, m, res = 0;
int ksm(int u, int v){
int ret = 1;
while(v){
if(v & 1) ret = ret * u % modd;
u = u * u % modd, v >>= 1;
}
return ret;
}
int fac[1000005], inv[1000005];
int C(int u, int v){
return (fac[u] * inv[v] % modd) * inv[u - v] % modd;
}
int f[1000005];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
fac[0] = inv[0] = 1;
for(int i = 1; i <= 1000000; i ++)
fac[i] = (fac[i - 1] * i) % modd;
inv[1000000] = ksm(fac[1000000], modd - 2);
for(int i = 999999; i >= 1; i --)
inv[i] = inv[i + 1] * (i + 1) % modd;
cin >> n;
if(n == 2) return cout << 1, 0;
if(n == 3) return cout << 4, 0;
m = n - 3;
int tk = m;
for(int i = 0; i <= m; i ++){
if(2 * i - 1 > m){
tk = i - 1;
break;
}
int tmp = C(m - i + 1, i);
f[i] = (tmp * fac[n - 1 - i] % modd) * fac[i] % modd;
}
for(int i = 0; i <= tk; i ++)
f[i] = (((f[i] - f[i + 1]) % modd) + modd) % modd;
for(int i = 0; i <= tk; i ++)
res = (res + (f[i] * (n - 1 - i) % modd)) % modd;
cout << res;
return 0;
}

record


[ABC201F] Insertion Sort

N 个人排成一列,他们的编号是 1N 的排列。左起第 i 个人的编号是 Pi

你可以以任意次序进行任意多次下列操作:

  • 选择一个人,设其编号为 i,支付 Ai 的代价将其移动到任意位置。
  • 选择一个人,设其编号为 i,支付 Bi 的代价将其移动到最左端。
  • 选择一个人,设其编号为 i,支付 Ci 的代价将其移动到最右端。

其中 Ai,Bi,Ci 由题目输入。

你的目标是使得所有人的编号从左至右递增。输出达成目标的最小代价。

  • 1  N  2 × 105
  • 1  Pi  N
  • 1  Ai,Bi,Ci  109
  • Pi  Pj (i  j)
  • 入力は全て整数

solution

很像 [AGC032D] Rotation Sort 啊。

首先一个数要么不操作,要么只操作一次。因为如果操作两次,那么完全可以忽略第一次操作。

有一个很显然的推论,就是不操作的数会形成一个上升序列。因为三种操作都不会改变不操作的数的相对位置。

这题和 Rotation Sort 的区别在于指定的区间只能是 [1,n],并且增加了任意移位置的操作。对于选择任意移动的数,移动没有顺序之分,也就是说进行完左移和右移操作后任意移动这些数也不会有影响,所以可以把这些数也当作不会位置的数。也就是有这样的一个推论,所有不操作的数和任意操作的数会形成上升序列。

懒了,明天写。


[ABC338G] evall

给一个由 123456789+* 组成的字符串 S,保证其构成算术表达式。

定义 eval(Si..j) 表示字符串中 ij 这一段所构成的表达式的值,构成表达式要求 SiSj 都是数字,若无法构成表达式则这一段的 eval0

i=1|S|j=i|S|eval(Si..j)998244353 取模的值。

Solution

感觉思路不难想,就是写起来有点麻烦。

ai 表示 Si 位置的数字。

因为乘法的优先级大于加法, 所以可以把原串里的加号当作分割点,先累加 l,r 在同一个连续的乘法段内的 eval(Sl..r),最后再处理 l,r 跨过加号的。

在连续的乘法段内的 l,r 也有两种情况,即 l,r 跨过了乘号或没有跨过乘号。

1. l,r 没有跨过乘号

简单的递推。设 fi 表示在前一个乘号到 i 这一段,以 i 结尾的字符串的 evalgi 表示前一个乘号与 i 间隔的数字数。则 gi=gi1+1,fi=10fi1+gi×ai

这一段的贡献即为 fii 的范围是当前考虑的纯数字的区间。

2. l,r 中跨过了乘号

hi 表示从这一个连续的乘法段的开头到 i 这一段,以 i 结尾的字符串的 eval

假设上一个乘号的前一个位置为 j,当前位置为 i,从 j+2 位置到 i 位置连起来形成的数是 now

那么有 hi=hj×now+fi

hi 递推出来后,这一段的贡献即为 hii 的范围是当前考虑的连续乘法段。

接下来处理加法。

记录 vali 表示 i 从所在的连续乘法段到 i 的子串得到的表达式的结果。设 Fi 表示从字符串开头到 i 这一段,以 i 结尾的字符串的 eval,再记录 cnt 为从开头到 i 中间的数字个数。

假设上一个加号的前一个位置为 j,当前位置为 i

那么有 Fi=Fj+cnt×vali+hi

最终答案即为 i=1nFi

然后就是注意一下取模,然后这题就做完了。

我的代码中将 f,h,F 的贡献分开计算了,所以与上面的过程上略有不同,不过大体思路是相同的。

code

#include<iostream>
#include<fstream>
#include<algorithm>
#include<string>
#define int long long
using namespace std;
namespace solve1{
int n;
const int modd = 998244353;
string s;
int l[1000005], r[1000005];
int val[1000005], f[1000005];
int h[1000005];
int ans;
int tot;
int main(){
cin >> s;
n = s.length();
s = " " + s;
tot = 1, l[1] = 1;
for(int i = 1; i <= n; i ++)
if(s[i] == '+') r[tot] = i - 1, l[++ tot] = i + 1;
r[tot] = n;
for(int k = 1; k <= tot; k ++){
int pre = 0, lst = 1, nw = 0, cnt = 0;
for(int i = l[k]; i <= r[k]; i ++){
if(s[i] == '*') pre = (pre * nw % modd + f[i - 1]) % modd, lst = (lst * nw % modd),
h[r[k]] = (h[r[k]] * nw % modd + f[i - 1]) % modd;
else if(i == l[k] || s[i - 1] == '*'){
f[i] = s[i] - '0', cnt = 1;
(ans += f[i]) %= modd;
nw = s[i] - '0';
val[i] = lst * nw % modd;
ans = (ans + pre * nw % modd) % modd;
}
else{
f[i] = (f[i - 1] * 10 + cnt * (s[i] - '0') % modd) % modd;
f[i] = (f[i] + (s[i] - '0')) % modd, cnt ++;
(ans += f[i]) %= modd;
nw = nw * 10 + (s[i] - '0');
nw %= modd;
ans = (ans + pre * nw % modd) % modd;
val[i] = lst * nw % modd;
}
}
h[r[k]] = (h[r[k]] * nw % modd + f[r[k]]) % modd;
}
int lt = 0, ct = 0, un0 = 0;
for(int i = 1; i <= n; i ++){
if(s[i] != '+' && s[i] != '*')
un0 ++, (ans += lt + ct * val[i] % modd) %= modd;
else if(s[i] == '+'){
lt = (lt + val[i - 1] * ct % modd + h[i - 1]) % modd;
ct = ct + un0, un0 = 0;
}
}
cout << ans;
return 0;
}
}
signed main(){
int T = 1;
while(T --)
solve1::main();
return 0;
}

[ARC159C] Permutation Addition

Description

给定一个长度为 n 正整数序列 A=(a1,a2,,an)

你需要进行如下操作 0104 次,使得 A 中的数全部相等:

  • 选择一个 (1,2,,n) 的排列 (p1,p2,,pn),将序列 A 变为 (a1+p1,a2+p2,,an+pn)

solution

一种比较简单的构造题。

首先对于一次操作,最后增加的总和一定是n(n+1)2

对于奇数而言,有 n(n+1)20(modn),对于偶数而言有 n(n+1)20(modn2)

那么如果不满足上面的那两个条件可以直接输出 NO

我们发现把序列加上一次 {1,2,3,4,,n},然后再加上一次 {n,n1,n2,,1},序列的相对大小没有发生变化。

那么如果交换一下前两个数,将序列加上 {2,1,3,4,,n},然后再同样的加上 {n,n1,n2,,1} ,那么我们会发现在第一个数小于第二个数的情况下,两个数的相对距离会减小 2

那么思路就很清晰了。不断的找序列最大的数和最小的数,用一次如上操作使两个数的距离减小,直到序列全部相等为止。

但是这样捆绑起来的两个操作会让序列整体增加 n,对于偶数而言,如果 n(n+1)20(modn2) 但是 n(n+1)20(modn),那么没法用这种方法变得全部相同。

解决方法很简单。只用先将序列随便加上一个排列使得 n(n+1)20(modn),再用如上算法就行了。

code

#include<iostream>
#include<fstream>
#include<algorithm>
using namespace std;
namespace solve1{
int n;
pair<int, int> a[55];
int ans[10005][55], tot;
int check(){
int c = a[1].first;
for(int i = 1; i <= n; i ++)
if(a[i].first != c) return false;
return true;
}
int main(){
cin >> n;
int sum = 0;
for(int i = 1; i <= n; i ++)
cin >> a[i].first, a[i].second = i, sum += a[i].first;
if(n % 2 == 1 && sum % n != 0) return cout << "No", 0;
if(n % 2 == 0 && sum % (n / 2) != 0) return cout << "No", 0;
if(n % 2 == 0 && sum % n != 0){
tot ++;
for(int i = 1; i <= n; i ++)
ans[tot][i] = i, a[i].first += i;
}
while(!check() && tot <= 10000){
sort(a + 1, a + n + 1);
swap(a[2], a[n]);
tot ++;
ans[tot][a[1].second] = 2, ans[tot][a[2].second] = 1;
a[1].first += 2, a[2].first ++;
for(int i = 3; i <= n; i ++){
a[i].first += i;
ans[tot][a[i].second] = i;
}
tot ++;
for(int i = 1; i <= n; i ++){
a[i].first += (n - i + 1);
ans[tot][a[i].second] = n - i + 1;
}
}
if(tot > 10000){
cout << "No";
return 0;
}
cout << "Yes\n";
cout << tot << "\n";
for(int i = 1; i <= tot; i ++){
for(int j = 1; j <= n; j ++)
cout << ans[i][j] << ' ';
cout << "\n";
}
return 0;
}
}
signed main(){
int T = 1;
while(T --)
solve1::main();
return 0;
}

本文作者:AzusidNya

本文链接:https://www.cnblogs.com/AzusidNya/p/17621444.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   AzusidNya  阅读(42)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起