【题解】Educational Codeforces Round 144(CF1796)
被 C 卡了。
最后被 E 的各种分讨劝退,但是同时也学习到了一种换根 dp 的简单写法。
评价:It's educational for me.
A.Typical Interview Problem
题目描述:
有一个包含 F 和 B 的字符串,最开始是空的。我们开始从 \(1\) 向后遍历所有整数,按照下面的规则构成字符串:
- 如果该整数能被 \(3\) 整除,则在该字符串后加入 F
- 如果该整数能被 \(5\) 整除,则在该字符串后加入 B
特别的,如果该整数能同时被 \(3\) 和 \(5\) 整除,则先加入 F 再加入 B。
现在有 \(t\) \((1\le t\le2046)\) 次询问,每个给出一个长度为 \(k\) \((1\le k \le 10)\) 的字符串 \(s\),请回答该字符串是否是上面字符串的子串(需要连续)。(保证给出的字符串仅包含 F 和 B)
题目分析:
构成的字符串显然是有循环节的,所以直接打表出来几个循环节,然后判断 \(s\) 是否出现就可以了,可以暴力判断。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
string s,t;
bool chk(int pos,string t){
for(int i=0; i<t.size(); i++){
if(t[i] != s[pos + i]) return false;
}
return true;
}
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
for(int i=1; i<=300; i++){
if(i % 3 == 0) s = s + 'F';
if(i % 5 == 0) s = s + 'B';
}
int T;scanf("%d",&T);
while(T--){
int len;
cin>>len>>t;
bool flag = false;
for(int i=0; i<s.size(); i++){
if(i + len - 1 < s.size() && chk(i,t)) flag = true;
}
if(flag) printf("Yes\n");
else printf("No\n");
}
return 0;
}
B.Asterisk-Minor Template
题目描述:
定义一个字符串的模板 \(T\),其中仅包含小写字母和可以代替任意字符串(包括空字符串)的通配符 '*'。一个合法的模板需要满足 '*' 的数量小于等于小写字母的数量。
有一个仅包含小写字母的字符串 \(s\),如果我们可以选择出一个合法的模板 \(T\),可以通过替换所有的 '*' 使其等于 \(s\),则称字符串 \(s\) 与模板 \(T\) 匹配。
现在有 \(t\) $(1 \le t \le 10^4) $ 组询问,每次给出两个仅包含小写字母的字符串 \(a,b\) \((1\le|a|,|b|\le 50)\),请给出一个合法的模板 \(T\) 使其能同时匹配两个字符串,或输出无解 "NO"。
题目分析:
对于一个模板 \(T\),其一定可以表示为 *c
、c*
、*cd*
的形式,所以可以对于每一个都判断一下就好了。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 100;
char s[N],t[N];
bool flag1[N][N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%d",&T);
while(T--){
scanf("%s%s",s+1,t+1);
int n = strlen(s+1);
int m = strlen(t+1);
bool flag = false;
for(int i=1; i<n; i++) flag1[s[i]-'a'][s[i+1]-'a'] = true;
for(int i=1; i<m; i++){
if(flag1[t[i]-'a'][t[i+1]-'a']){
flag = true;
printf("Yes\n*%c%c*\n",t[i],t[i+1]);
break;
}
}
if(s[1] == t[1] && !flag) printf("Yes\n%c*\n",s[1]),flag = true;
if(s[n] == t[m] && !flag) printf("Yes\n*%c\n",t[m]),flag = true;
if(!flag) printf("No\n");
for(int i=1; i<n; i++) flag1[s[i]-'a'][s[i+1]-'a'] = false;
}
return 0;
}
C.Maximum Set
题目描述:
定义一个集合 \(S\) 是合法的,当且仅当集合中任意两个整数 \(x\) 和 \(y\) 满足 \(x\) 被 \(y\) 整除或 \(y\) 被 \(x\) 整除。
有 \(t\) \((1\le t\le 2\times10^4)\) 次询问,每次给出整数 \(l,r\) \((1\le l\le r \le 10^6)\),每次询问给出两个回答,第一个是在集合 \([l,r]\) 中选出整数组成的合法集合的最大大小,第二个是大小最大的合法集合的个数。
题目分析:
集合里面的数肯定是由最小值每次乘某个数然后扩展得到的。
为了使得集合大小最大,显然只能乘 \(2\) 或者少量的 \(3\),因为如果乘 \(4\) 就显然不如乘两次 \(2\),这样可以让集合大小加 \(1\)。
所以可以直接通过 \(l\) 一直乘 \(2\) 进行扩展,这样就可以得到集合最大的大小,然后一个个地将乘 \(2\) 替换为乘 \(3\),计算并计算有多少个最小值符合条件。
这样为什么是合法的,考虑在我们一步步替换的过程中集合的大小不会变,集合大小其实就是质因数分解之后的指数之和,而我们指数最多为 \(O(\log n)\) 所以可以暴力枚举。
可以发现若当前乘的值为 \(p\),那么最小值 \(x\) 满足条件当且仅当:\(x \in [l,r]\) 且 \(xp \in [l,r]\),化简一下就是 \(x \in [l,\lfloor \frac{r}{p} \rfloor]\)。
需要注意的一点就是,我们乘 \(2\) 和乘 \(3\) 不同的顺序会造成集合的不同,先乘 \(2\) 和先乘 \(3\) 并不一样,所以贡献要带一个组合数。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
int C[105][105];
int binom(int n,int m){
if(n < m || n < 0 || m < 0) return 0;
return C[n][m];
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
C[0][0] = 1;
for(int i=1; i<=100; i++){
C[i][0] = 1;
for(int j=1; j<=i; j++){
C[i][j] = (C[i-1][j] + C[i-1][j-1])%MOD;
}
}
int T;scanf("%lld",&T);
while(T--){
int l,r;scanf("%lld%lld",&l,&r);
int tmp = 1,cnt = 0;
while(tmp * l <= r) tmp *= 2,cnt++;
--cnt;tmp /= 2;
int ans = 0;
ans = (ans + r/tmp - l + 1)%MOD;
for(int i=1; i<=cnt; i++){
tmp = tmp / 2 * 3;
if(l * tmp > r) break;
ans = (ans + (r/tmp - l + 1) * binom(cnt,i))%MOD;
}
printf("%lld %lld\n",cnt+1,ans);
}
return 0;
}
D.Maximum Subarray
题目描述:
你得到了一个序列 \(a_1,a_2,\cdots,a_n\),由 \(n\) 个整数组成。并且你还得到了两个整数 \(k,x\)。
你需要执行一次操作:选择恰好 \(k\) 个不同的位置加上 \(x\),其余位置减去 \(x\)。
比如说:如果 \(a=[2,-1,2,3],k=1,x=2\),然后我们选择第一个位置,操作之后的 \(a=[4,-3,0,1]\)。
定义 \(f(a)\) 为 \(a\) 序列的子串的最大可能和。\(a\) 的子串是 \(a\) 的一部分,即序列 \(a_i,a_{i+1},\cdots a_j\) 对于某个 \(1\le i\le j\le n\)。同时,空数组也应该被考虑,它的和为 \(0\)。
让 \(a'\) 为 \(a\) 操作后的序列,输出 \(f(a')\) 的最大可能值。
\(1\le t\le 10^4;\;1\le n,\sum n\le 2\cdot 10^5;\;0\le k\le \min(20,n);\;-10^9\le a_i,x\le 10^9\)
题目分析:
认为 \(x\) 一定为正数,因为若 \(x\) 为负数显然可以 \(-x \to x,n-k \to k\),就能让 \(x\) 变为正数。
若钦定区间 \([l,r]\) 为我们选择的最优区间,则若区间长度小于 \(k\) 则只能加区间长度个 \(x\),否则只能加 \(k\) 个 \(x\) 并且还有一些位置必须加 \(-x\)。
所以下面的一个想法就是,因为要考虑每一个子串,所以枚举右端点 \(r\)。
这样我们就可以对于不同的区间的长度进行分类讨论不同的贡献。
若区间长度小于 \(k\),注意到 \([l,r]\) 的区间和可以写为 \(s_r - s_{l-1}\),所以只需要预先对每个位置都加 \(x\),这样一个长度小于等于 \(k\) 的区间的权值和就对了。
若区间长度大于等于 \(k\),因为减 \(x\) 的数量与区间长度有关,而加 \(x\) 的数量一定,所以不妨将每个位置都减 \(x\),这样只需要将对应的区间和加 \(2kx\) 就是正确的权值和。
每一个都相当于一个一定长度的最大子段和,就相当于 \(s_r - s_{l-1}\) 中固定了 \(s_r\) 求 \(l\) 在一定范围内的 \(s_{l-1}\) 的最小值。
这个可以使用 ST 表维护,复杂度 \(O(n \log n)\)。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int INF = 1e18+5;
int st1[N][22],st2[N][22];
int pre1[N],pre2[N];
int query1(int l,int r){
if(l > r) return INF;
int len = log2(r-l+1);
return min(st1[l][len],st1[r-(1<<len)+1][len]);
}
int query2(int l,int r){
if(l > r) return INF;
int len = log2(r-l+1);
return min(st2[l][len],st2[r-(1<<len)+1][len]);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
memset(st1,0x3f,sizeof(st1));
memset(st2,0x3f,sizeof(st2));
int T;scanf("%lld",&T);
while(T--){
int n,k,x;scanf("%lld%lld%lld",&n,&k,&x);
if(x < 0) x = -x,k = n - k;
st1[0][0] = 0,st2[0][0] = 0;
for(int i=1; i<=n; i++){
int a;scanf("%lld",&a);
pre1[i] = pre1[i-1] - x + a;
pre2[i] = pre2[i-1] + x + a;
st1[i][0] = pre1[i];
st2[i][0] = pre2[i];
}
for(int i=1; i<=20; i++){
for(int j=0; j+(1<<i)-1<=n; j++){
st1[j][i] = min(st1[j][i-1],st1[j+(1<<(i-1))][i-1]);
st2[j][i] = min(st2[j][i-1],st2[j+(1<<(i-1))][i-1]);
}
}
int ans = 0;
for(int i=1; i<=n; i++){
if(i >= k){
int tmp = query1(0,i-k);
ans = max(ans,pre1[i] - tmp + 2 * x * k);
}
int tmp = query2(max(i-k+1,0ll),i);
ans = max(ans,pre2[i] - tmp);
}
printf("%lld\n",ans);
for(int i=0; i<=20; i++){
for(int j=0; j+(1<<i)-1<=n; j++){
st1[j][i] = INF;
st2[j][i] = INF;
}
}
}
return 0;
}
E.Colored Subgraphs
题目描述:
Alice有一棵 \(n\) 个节点的树。
他要选择一个节点 \(r\),然后:
- 设一个数组 \(d\),\(d_i\) 表示 \(r\) 点到 \(i\) 点的距离。
- 并把所有节点都染个色。
但染色必须要满足两个条件:
- 对于任意被染了相同颜色的点对 \((x,y)\),从点 \(x\) 到点 \(y\) 的路径上所有点(包括点 \(x\) 和点 \(y\))的颜色都一样。
- 对于任意被染了相同颜色的点对 \((x,y)\),条件 \(d_x \not= d_y\) 必须都满足。
现在,定义一个染色方案的代价为一种颜色的最小出现次数,即 \(\min(\text{cnt}_i)\)。
现在,他让你选择一个最优的节点 \(r\),使得染色方案的最小代价最大。
\(1 \le n \le 2\times 10^5\)
题目分析:
要解决这个问题显然需要换根 \(dp\),所以现在的问题就是给定一个 \(r\) 怎么求解答案。
考虑一个贪心的想法就是每种颜色从叶子向上扩展,当某一个节点有多个儿子,则选择所在的链长度最短的一个儿子的颜色作为当前点的颜色,然后向上扩展这一条链。
这个过程放到 \(dp\) 下理解就是,设 \(f_u\) 表示以 \(u\) 为根的子树按照上述过程操作之后的 \(u\) 所在的链长度。
转移的话显然就是从儿子里选一个最小的转移,没被选择的部分对答案造成贡献,而显然对答案造成贡献的就是所有儿子的次小值,所以只需要记一下次小值。
而换根的话就是考虑根由 \(u \to v\),那么就将 \(v\) 对 \(u\) 的贡献删去,然后加入 \(u\) 对 \(v\) 的贡献,为了维护次小值就必须同时维护第三小值,就比较爆炸。
一个很好写的做法就是,对于每个点 \(u\) 维护 \(f_u\) 表示 \(u\) 所有儿子所在链的长度,这样对 \(u\) 父亲的贡献就是 \(f_u\) 中的最小值,对答案的贡献就是 \(f_u\) 中的次小值,关键是换根的之后只需要在 \(f_u\) 中删去 \(v\) 的贡献然后在 \(f_v\) 中加入 \(u\) 的贡献即可,省去了所有的分类讨论,产生的影响只是复杂度多了一个 \(O(\log n)\),而在一般的换根 \(dp\) 中 \(O(n)\) 与 \(O(n \log n)\) 显然没区别。
要注意的就是根对答案的贡献为 \(f\) 中的最小值。
在代码实现的时候我默认 \(f_u\) 中的所有状态不包含 \(u\) 这个点,所以要注意一下细节。
代码:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const LL mod = 1e9 + 7;
const int N = 200005;
vector<int> G[N];
multiset<int> f[N];
multiset<int> se;
int ans;
int getlen(int u) {
return f[u].size() == 0 ? 1 : *f[u].begin() + 1;
}
void add(int u, int val) {
if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
f[u].insert(val);
if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}
void del(int u, int val) {
if (f[u].size() >= 2) se.erase(se.find(*next(f[u].begin())));
f[u].erase(f[u].find(val));
if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}
void dfs1(int u, int fa) {
for (auto v : G[u]) {
if (v == fa) continue;
dfs1(v, u);
f[u].insert(getlen(v));
}
if (f[u].size() >= 2) se.insert(*next(f[u].begin()));
}
void dfs2(int u, int fa) {
ans = max(ans, min(getlen(u), se.empty() ? INF : *se.begin()));
for (auto v : G[u]) {
if (v == fa) continue;
del(u, getlen(v));
add(v, getlen(u));
dfs2(v, u);
del(v, getlen(u));
add(u, getlen(v));
}
}
int main() {
int _;
scanf("%d", &_);
while (_--) {
int n;
scanf("%d", &n);
se.clear();
for (int i = 1; i <= n; i++) G[i].clear(), f[i].clear();
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
ans = 0;
dfs1(1, 0);
dfs2(1, 0);
printf("%d\n", ans);
}
return 0;
}