概述
省流:最后1个小时过掉T3,写了T4暴力。
得分: 100 + 100 + 100 + 65 = 365
rk1
题解
A. 谁开了小号(随机化)
题面:
分析:
用随机化过的。
只有四场比赛,考虑对于每个账号,存储状态 m a s k i mask_i maski 表示 i i i 参加了那些比赛。那么 m a s k i ∈ [ 0 , 15 ] mask_i \in [0, 15] maski∈[0,15]。若干账号可以合并为一个的条件是任意两个账号的 m a s k mask mask 与 起来为 0 0 0。
但是我们不知道按照怎样的优先级去合并是最优的。比如
(
1001
)
2
(1001)_2
(1001)2 和
(
1000
)
2
(1000)_2
(1000)2 都可以与
(
0110
)
2
(0110)_2
(0110)2 合并,但是如果还有一个账号的
m
a
s
k
mask
mask 为
(
0111
)
2
(0111)_2
(0111)2,那么显然
(
0110
)
2
(0110)_2
(0110)2 与
(
1001
)
2
(1001)_2
(1001)2 合并更好。这个顺序不好求出(实际上是我不会 )。那么我们可以 随机化。即随机一个优先合并的顺序。
我们将 [ 0 , 15 ] [0, 15] [0,15] 随机排列,考虑拿当前状态为 i i i 的和前面状态为 j j j 的元素合并,如果能合并,就让 c n t i ∣ j cnt_{i | j} cnti∣j 加上 m i n ( c n t i , c n t j ) min(cnt_i, cnt_j) min(cnti,cntj)。 i ∣ j i|j i∣j 一定大于 i i i,因此如果能接着合并会在后面考虑到。
由于只有 16 16 16 个数,因此多次随机能将最优顺序搞出来。答案很容易正确。
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, mask[N], c[N], cnt[20], id[20], res, tmp[20], w;
int main() {
scanf("%d", &n);
for(int i = 1; i <= 4; i ++ ) {
scanf("%d", &m);
for(int j = 1; j <= m; j ++ ) {
scanf("%d", &c[j]);
mask[c[j]] |= (1 << (i - 1));
}
}
for(int i = 1; i <= n; i ++ ) {
cnt[mask[i]] ++;
}
int w = 0;
if(cnt[0]) {
res = cnt[0] - 1;
cnt[0] = 1;
w = res;
}
for(int i = 0; i < (1 << 4); i ++ ) id[i] = i;
while(clock() < CLOCKS_PER_SEC * 0.99) {
random_shuffle(id, id + 16);
for(int i = 0; i < (1 << 4); i ++ ) tmp[i] = cnt[i];
int temp = w;
for(int i = 0; i < 16; i ++ ) {
for(int j = 0; j < 16; j ++ ) {
int val = id[j];
if(((i & val) == 0) && (i != val) && (tmp[i] > 0 && tmp[val] > 0)) {
int mn = min(tmp[i], tmp[val]);
temp += mn;
tmp[i] -= mn; tmp[val] -= mn;
tmp[i | val] += mn;
}
}
}
res = max(res, temp);
}
cout << res << endl;
return 0;
}
B. 终端命令(trie树)
分析:
称 s 0 ∼ n − 1 s_{0 \sim n-1} s0∼n−1 为 功能串, S S S 为 目标串。考虑如果当前手上有一个字符串,那么 它一定是目标串或至少一个功能串的前缀。如果不是的话将永远不能变成目标串。
将所有功能串和目标串插入 t r i e trie trie 树中。对于每个节点 u u u,把它和它出边连的点 v v v 连一条长度为 1 1 1 的 有向边,这表示添加字符操作。对于 t a p tap tap 操作,可以在插入功能串时对路径上的点打上标记。因为功能串是有序给出的,因此一个点可以到达第一次标记它的功能串的结尾点。对于功能串结尾点,直接连下一个功能串结尾点即可。
然后跑 d i j k s t r a dijkstra dijkstra 或者 0 / 1 b f s 0/1bfs 0/1bfs
时间复杂度 O ( ∑ ∣ s i ∣ × l o g ( ∑ ∣ s i ∣ ) ) O(\sum|s_i| \times log(\sum|s_i|)) O(∑∣si∣×log(∑∣si∣))
CODE:
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 2e5 + 10;
const int M = 1e6 + 10;
int n, tr[M + N][26], id[M + N], tot, To, ed[N], head[N + M], len;
int ID[N + M], dis[N + M];
char S[N], t[M];
bool bok[N + M];
vector< int > E[N + M];
void add(int u, int v) {
E[u].pb(v);
}
void ins(char *str, int idx) {
int len = strlen(str + 1);
int p = 0;
for(int i = 1; i <= len; i ++ ) {
if(!tr[p][str[i] - 'a']) {
tr[p][str[i] - 'a'] = ++ tot;
add(p, tr[p][str[i] - 'a']);
}
if(!id[p] && idx != -1) id[p] = idx;
p = tr[p][str[i] - 'a'];
}
if(idx != -1) {
ed[idx] = p;
bok[p] = 1;
ID[p] = idx;
}
else To = p;
}
struct state {
int x, w;
friend bool operator < (state a, state b) {
return a.w > b.w;
}
};
bool vis[N + M];
priority_queue< state > q;
void dijkstra(int s) {
memset(dis, 0x3f, sizeof dis);
dis[s] = 0; q.push((state) {s, dis[s]});
while(!q.empty()) {
state u = q.top(); q.pop();
int x = u.x;
if(vis[x]) continue;
vis[x] = 1;
for(auto v : E[x]) {
if(dis[v] > dis[x] + 1) {
dis[v] = dis[x] + 1;
q.push((state) {v, dis[v]});
}
}
}
}
int main() {
scanf("%d", &n);
scanf("%s", S + 1);
for(int i = 1; i <= n; i ++ ) {
scanf("%s", t + 1);
ins(t, i);
}
ins(S, -1);
for(int i = 0; i <= tot; i ++ ) {
if(!bok[i]) {
if(id[i] > 0) add(i, ed[id[i]]);
}
else {
int o = ID[i];
o ++;
if(o == n + 1) o = 1;
add(i, ed[o]);
}
}
dijkstra(0);
printf("%d\n", dis[To]);
return 0;
}
C. 又见LIS(dp)
分析:
这题卡了好长时间,最后在 30 m i n 30min 30min 内写完了。
这里有一个关键性质: Lis数组不会突变。因此对于当前位置而言,设前面 L i s Lis Lis 数组的最大值为 x x x,那么当前可填数的范围就是 [ 1 , x + 1 ] [1, x + 1] [1,x+1]。
具体的一种构造 v i v_i vi 方式可以 让 v i v_i vi 和 L i s i Lis_i Lisi 相等。那么在满足上述条件的基础上任意构造 L i s Lis Lis 数组都是合法的。
这下我们有了一个 d p dp dp 状态: f i , j f_{i, j} fi,j 表示考虑了前 i i i 个位置,最大的 L i s Lis Lis 的值为 j j j,所有关心位置 取值的方案数。
分情况讨论转移:
- a i = 0 a_i = 0 ai=0
d
p
i
,
j
←
d
p
i
−
1
,
j
×
j
dp_{i, j} \gets dp_{i - 1, j} \times j
dpi,j←dpi−1,j×j
d
p
i
,
j
+
1
←
d
p
i
−
1
,
j
dp_{i, j + 1} \gets dp_{i - 1, j}
dpi,j+1←dpi−1,j
分别表示 不成为最大值 和 成为最大值 的两种转移。
- a i > 0 a_i > 0 ai>0
d p i , j ← d p i − 1 , j ( j ≥ a i ) dp_{i, j} \gets dp_{i - 1, j}(j \geq a_i) dpi,j←dpi−1,j(j≥ai)
这个相当于是 舍去一些不合法的方案。
- a i = − 1 a_i = -1 ai=−1
d p i , j + 1 ← d p i − 1 , j dp_{i, j + 1} \gets dp_{i - 1, j} dpi,j+1←dpi−1,j
这个我认为是最难想的:由于我们不关心这个位置的取值,所以把它按照 a i = 0 a_i = 0 ai=0 的方式转移一定会重复。考虑 将所有上一个状态 唯一对应 转移给一个当前状态。当前层的状态所存储的方案就是不重复的。为了不遗漏,所以我们想让所有合法状态在后面都尽可能被转移,因此我们将 a i a_i ai 设为 j + 1 j + 1 j+1 进行转移。
用有向图表示转移如下:
a
i
=
0
a_i = 0
ai=0
a
i
=
−
1
a_i = -1
ai=−1
时间复杂度 O ( n 2 ) O(n^2) O(n2)
CODE:
#include<bits/stdc++.h> // -1 全部接成大的 这样能保证不重复
using namespace std;
const int N = 5100;
typedef long long LL;
const LL mod = 998244353;
LL dp[N][N];
int n, a[N];
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i ++ ) {
scanf("%d", &a[i]);
}
dp[0][0] = 1LL;
for(int i = 0; i < n; i ++ ) {
for(int j = 0; j <= i; j ++ ) {
if(a[i + 1] == 0) {
if(j != 0) dp[i + 1][j] = (dp[i + 1][j] + dp[i][j] * (1LL * j) % mod) % mod; // 接在里面
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // 接后面
}
else if(a[i + 1] == -1) {
dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // 规定 -1 只接大的
}
else {
if(j + 1 >= a[i + 1]) {
if(j + 1 == a[i + 1]) dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod;
else dp[i + 1][j] = (dp[i + 1][j] + dp[i][j]) % mod;
}
}
}
}
LL res = 0;
for(int i = 1; i <= n; i ++ ) {
res = (res + dp[n][i]) % mod;
}
cout << res << endl;
return 0;
}
D. 飞一飞呀(组合数学)
分析:
这题有两种做法:转化坐标系 和 组合意义推导
首先 N 2 N^2 N2 做法是简单的:设 x , y , z x, y, z x,y,z 分别表示在 x x x 轴, y y y 轴, z z z 轴的移动步数。枚举 x x x 和 y y y,那么 z z z 可以求出。对 x , y , z x,y,z x,y,z 分别组合数求方案然后相乘就好了。
下面是正解:
S o l 1 : Sol_1: Sol1: 变换坐标系
这里有一个套路:
首先思考在一个二维平面上,你要走 n n n 步,从 ( 0 , 0 ) (0, 0) (0,0) 走到 ( X , Y ) (X, Y) (X,Y)。每次可以通过四种坐标变换
( + 1 , 0 ) (+1, 0) (+1,0), ( − 1 , 0 ) (-1, 0) (−1,0), ( 0 , + 1 ) (0, +1) (0,+1), ( 0 , − 1 ) (0, -1) (0,−1)
问方案数。
我们将坐标系变换一下,将 ( x , y ) (x, y) (x,y) 点映射到 ( x + y , x − y ) (x + y, x - y) (x+y,x−y) 上。不难发现这样的映射是一 一对应的,即 原来的一个点唯一对应变换后的一个点,变换后的一个点也唯一对应原来的一个点。 那么原来的四种坐标变换 在变换后的坐标系 就对应成了:
( + 1 , + 1 ) (+1, +1) (+1,+1), ( − 1 , − 1 ) (-1, -1) (−1,−1), ( + 1 , − 1 ) (+1, -1) (+1,−1), ( − 1 , + 1 ) (-1, +1) (−1,+1)
起点是 ( 0 , 0 ) (0, 0) (0,0),终点是 ( X + Y , X − Y ) (X + Y, X - Y) (X+Y,X−Y)。
我们发现 x x x轴 和 y y y 轴 的变换独立了。因此分别求一个组合数相乘即可。时间复杂度 O ( 1 ) O(1) O(1)。
放到这个问题上:我们发现三维中只有 6 6 6 种坐标变换,想让每个轴上的移动独立需要 9 9 9 种变换。因此不能拓展二维的方法。但是我们可以枚举一维,那么另外两维可以用上述方法 O ( 1 ) O(1) O(1) 求出。
时间复杂度 O ( N ) O(N) O(N)
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e7 + 10;
typedef long long LL;
const LL mod = 998244353;
int n, X, Y, Z;
LL res, fac[N], inv[N];
LL Pow(LL x, LL y) {
LL res = 1LL, k = x % mod;
while(y) {
if(y & 1) res = (res * k) % mod;
y >>= 1;
k = (k * k) % mod;
}
return res;
}
LL C(int n, int m) {
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
LL Cnt(int x) {
LL step = (x - Z) / 2;
return C(n, x) * C(x, Z + step) % mod;
}
LL calc(int x) {
LL p = abs(X - Y), q = abs(X + Y);
if(x < p || x < q) return 0;
if(((x - p) & 1) || ((x - q) & 1)) return 0;
return C(x, p + (x - p) / 2) % mod * C(x, q + (x - q) / 2) % mod;
}
int main() {
fac[0] = 1LL;
for(int i = 1; i < N; i ++ ) fac[i] = (fac[i - 1] * (1LL * i)) % mod;
inv[N - 1] = Pow(fac[N - 1], mod - 2LL) % mod;
for(int i = N - 2; i >= 0; i -- ) inv[i] = inv[i + 1] * (1LL * i + 1LL) % mod;
cin >> n >> X >> Y >> Z;
X = abs(X); Y = abs(Y); Z = abs(Z);
for(int i = Z; i <= n; i += 2) {
res = (res + Cnt(i) * calc(n - i) % mod) % mod;
}
cout << res << endl;
return 0;
}
S o l 2 : Sol_2: Sol2: 组合意义
感觉很神奇,不想推了。参考下面文章吧。