Atcoder Beginner 246 FG EX
本文最后更新于:2 年前
F - typewriter
题意:给出 $n$ 个字符串和一个长度 $L$,接下来你可以任选一个字符串,并且只用这个串内有的字符来打字(不限制字符顺序,数量),问可以打出多少中长度为 $L$ 的字符串。数据范围 $N<18$,$L<10^9$
第一种想法(没a):可以暴力枚举所选的字符集,一共 $2^{26}$ 种情况,先预处理出每一种字符在哪些给定的串中出现(bitmask),把所选的字符集的出现位置与一下,如果与的结果不为0说明这一字符集是可选的,就加上使用这个字符集的答案。为了避免重复,还要求这个字符集内的每一个字符都必须用到。假设字符集有 $k$ 个,那么答案就是将 $L$ 个位置分配给 $k$ 个字符,每个字符必须分配到至少一个位置。
本以为这个应该是一个公式解决,但查了一下发现是第二类斯特林数,递推也要 $O(L)$,目前我没法记录这个值。
第二种做法:暴力容斥,枚举选取了哪些串,计算使用同时被这些串所拥有的字符集的答案,如果选择了奇数个串答案就加上,否则答案就减去。
ll qpow(ll x, ll p){
ll r = 1;
while(p > 0){
if(p & 1) r = r * x % mode;
x = x * x % mode;
p /= 2;
}
return r;
}
int vis[27];
ll res[27], ans = 0;
void solve(){
int n, l;
cin >> n >> l;
for(int i=0;i<=26;i++) res[i] = qpow(i, l);
for(int i=1;i<=n;i++){
string s;
cin >> s;
for(char c: s) vis[i] |= (1 << (c - 'a'));
}
for(int i=1;i<(1<<n);i++){
int st = (1 << 26) - 1, f = -1;
for(int k=1;k<=n;k++){
if(i & (1 << (k-1))){
st = st & vis[k];
f *= -1;
}
}
int cnt = 0;
for(int k=0;k<26;k++) if(st & (1 << k)) cnt++;
(ans += f * res[cnt]) %= mode;
}
cout << (ans % mode + mode) % mode << '\n';
}
G - Game on Tree 3
题意:有一棵树,每个点有权值。Alice和Bob在玩游戏,一开始Bob在树根节点上,接下来轮流行动:
- Alice选择任意一个节点,将其权值清零
- Bob选择一条相邻的边,移动到一棵子树上(不可返回)
Bob可以随时结束游戏,问Bob能走到的最大点权是多少(Alice会让这个值尽可能小)
首先二分答案,假设答案为 $k$,那么Alice就要在Bob走到任意大于 $k$ 的节点之前,清空这些节点。
再考虑树上dp,设 $dp[i]$ 表示:当Bob走到 $i$ 这个子树上后,Alice需要在这个子树上操作至少多少次才能阻止Bob
这么设置状态的原因是Alice可以提前处理一些比较远的点(在Bob目前没有威胁时)
转移时:$dp[u] = \sum dp[v]-1+(val[v]>k)$
如果 $dp[1]>1$,说明Alice无法阻止Bob,继续二分调整答案。
int n, val[maxn];
vector<int> vp[maxn];
int f[maxn];
void dfs(int p, int fa, int lim){
f[p] = 0;
for(int v: vp[p]){
if(v != fa){
dfs(v, p, lim);
f[p] += max(0, f[v]-1) + (val[v] > lim);
}
}
}
void solve(){
cin >> n;
for(int i=2;i<=n;i++) cin >> val[i];
for(int i=1;i<n;i++){
int u, v;
cin >> u >> v;
vp[u].push_back(v);
vp[v].push_back(u);
}
int l = 0, r = 1e9;
while(l < r){
int mid = (l + r) / 2;
dfs(1, -1, mid);
if(f[1] > 1){
l = mid + 1;
}else{
r = mid;
}
}
cout << l << '\n';
}
Ex - 01? Queries
题意:给出一个长度为 $n$ 的序列,仅包含 0,1,?
,接下来有 $Q$ 次操作,每次选择一个位置,将其修改为0,1,?
中的一个,然后问你:当前序列包含多少种不同的子序列,?
可以视为 0
或 1
先不考虑修改,单独对一个序列如何求解:
设 $dp[i][0/1]$ 表示前 $i$ 位上,末尾为 $0/1$ 的子序列个数,转移时:
- 若 $s[i]=0$,则 $dp[i][0]=dp[i-1][0]+dp[i-1][1]+1$,否则 $dp[i][0]=dp[i-1][0]$
- 若 $s[i]=1$,则 $dp[i][1]=dp[i-1][0]+dp[i-1][1]+1$,否则 $dp[i][1]=dp[i-1][1]$
?
则两者均成立
这种对子序列的dp没怎么见过,感觉挺玄妙的,以 $dp[i][0]=dp[i-1][0]+dp[i-1][1]+1$ 为例,我的理解的思路是:
- 先继承上一位的结果: $dp[i-1][0]$
- 然后对上一位的每一个子序列,都在末尾加上一个0,即加上:$dp[i-1][0]+dp[i-1][1]+1$(包括空串加0)
- 然后减去重复部分,显然,上述第一部分是完全被第二部分包含的,那么直接把第一部分去掉即可。
有了这个dp式子之后呢?怎么处理询问?
这个dp形式比较简单,可以考虑用矩阵来表示,还是以当前位为0举例:
$$
\left[
\begin{matrix}
dp[i-1][0] & dp[i-1][1] & 1 \
\end{matrix}
\right]
\times
\left[
\begin{matrix}
1 & 0 & 0 \
1 & 1 & 0 \
1 & 0 & 1
\end{matrix}
\right]
=
\left[
\begin{matrix}
dp[i][0] & dp[i][1] & 1 \
\end{matrix}
\right]
$$
同样可以把1和?表示成矩阵。我们要做单点修改矩阵值,查询整段矩阵乘积,可以用线段树维护。
struct mat{
int a[3][3];
mat operator *(const mat &t)const{
mat res = {};
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
for(int k=0;k<3;k++){
(res.a[i][j] += 1ll * a[i][k] * t.a[k][j] % mode) %= mode;
}
}
}
return res;
}
};
const mat m[3] = {
{1, 0, 0,
1, 1, 0,
1, 0, 1},
{1, 1, 0,
0, 1, 0,
0, 1, 1},
{1, 1, 0,
1, 1, 0,
1, 1, 1}
};
map<char, int> fff{{'0', 0}, {'1', 1}, {'?', 2}};
string s;
struct segTree{
mat sum[maxn << 2];
void push_up(int p){
sum[p] = sum[p*2] * sum[p*2+1];
}
void modify(int p, int l, int r, int pos, int t){
if(l == r){
sum[p] = m[t];
return;
}
int mid = (l + r) / 2;
if(pos <= mid) modify(p*2, l, mid, pos, t);
else modify(p*2+1, mid+1, r, pos, t);
push_up(p);
}
void build(int p, int l, int r){
if(l == r){
sum[p] = m[fff[s[l-1]]];
return;
}
int mid = (l + r) / 2;
build(p*2, l, mid);
build(p*2+1, mid+1, r);
push_up(p);
}
}segt;
void solve(){
int n, q;
cin >> n >> q;
cin >> s;
segt.build(1, 1, n);
for(int i=1;i<=q;i++){
int x; char c;
cin >> x >> c;
segt.modify(1, 1, n, x, fff[c]);
mat res = mat{0, 0, 1,
0, 0, 0,
0, 0, 0} * segt.sum[1];
cout << (1ll * res.a[0][0] + res.a[0][1]) % mode << '\n';
}
}