复健 Day 1: AC 3/6
- D: 概率与期望
- E: 树形DP,线性基,DFS序
- F: 树形DP,记忆化搜索
传送门
A. Flip Flop Sum
输入一个长度为$n$的数组$a$,每个元素$a_i = 1 / -1$。你有且只有一次机会将$a_i$与$a_{i+1}$取相反数,求操作后的$max(\sum\limits_{i=1}^{n} a_i)$。
签到题,出现两个相邻负数直接取反和+4,否则若出现一正一负取反和不变,都没有仅可能全是整数,和-4。时间复杂度$O(n)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <cstdio> #include <algorithm> using namespace std; int a[100010]; int main() { int T,n; scanf("%d",&T); while(T--) { bool flag1=false, flag2=false; long long sum=0; scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",a+i), sum+=a[i]; for(int i=2;i<=n;++i) { if(a[i]==-1&&a[i]==a[i-1]) flag1=true; if(a[i]^a[i-1]) flag2=true; } if(flag1) printf("%lld\n", sum+4); else if(flag2) printf("%lld\n", sum); else printf("%lld\n",sum-4); } return 0; }
|
B. The Forbidden Permutation
输入一个长度为$n$的排列$p_i$与一个长度为$m$,每个元素$\leq n$的数组$a$。如果对每个$a_i$都有$pos(a_i) \lt pos(a_{i+1}) \le pos(a_i)+d$,则称数组$a$被定义为坏数组。你需要执行操作,每次操作可以将$p_i$与$p_{i+1}$交换,问至少需要多少次操作才能使数组$a$变成好数组。
坏数组的性质非常特殊:输入的数组$a$是坏数组,则每个$a_i$在$p$中的顺序应当与在$a$中顺序一样,且两两之间差距不超过$d$。将$a_i$挂在$p_i$上,则每次对$p_i$操作都可以改变相邻$a_i$的间隔。只需要让一对相邻的$a_i$距离小于$0$(反序)或大于$d$即可。故$ans=min(min(d-(pos(a_{i+1})-pos(a_i))+1),min(pos(a_{i+1})-pos(a_i)))$.
如果排列长度$n \le d+1$,则不能操作让距离大于$d$,特判即可。时间复杂度$O(n+m)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <cstdio> #include <algorithm> using namespace std; #define MAXN 100010 int p[MAXN],a[MAXN],ppos[MAXN],apos[MAXN]; int main() { int T,n,m,d; scanf("%d",&T); while(T--) { int ans=0x3f3f3f3f; scanf("%d%d%d",&n,&m,&d); for(int i=1;i<=n;++i) scanf("%d",p+i),ppos[p[i]]=i; for(int i=1;i<=m;++i) scanf("%d",a+i),apos[i]=ppos[a[i]]; for(int i=2;i<=m;++i) { ans=min(ans,apos[i]-apos[i-1]); if(d+1<n) ans=min(ans,d-(apos[i]-apos[i-1])+1); } printf("%d\n",max(ans,0)); } return 0; }
|
C. Flexible String
输入两个字符串$a$与$b$,其中$a$所含不同字符的个数$\le 10$。现可以将$a$中最高不超过$k$种字符任意转换成其他字符(决定转换后不同位置的同种字符可转换为不同字符),问转换后的$a$字符串可与$b$字符串最多有多少相同子串。(子串计算方法可重叠,即有多少$(l,r)$使得$a[l,r]=b[l,r]$)
假设我们已经确定转换的字符种类,那么每个可转换字符转换目标肯定是$b$字符串对应字符。转换后,求出所有不重叠的相同子串长度$l_i$,易知$ans=\sum \frac{l_i (l_i+1)}{2}$。
字符串$a$只包含$\le 10$种不同字符,则可能的转换方案有$C_m^k \le C_{10}^{5}$种,枚举的时间复杂度是正确的,搜索即可。时间复杂度$O(C_m^k n)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include <cstdio> #include <cstring> #include <algorithm> using namespace std; #define MAXN 100010 int n,k; char c[11]; bool canmod[26]; char a[MAXN],b[MAXN]; long long ans; void getans() { long long ret=0,tmp=0; for(int i=1;i<=n;++i) { if(canmod[a[i]-'a']||a[i]==b[i]) ++tmp; else ret+=tmp*(tmp+1)/2,tmp=0; } ret+=tmp*(tmp+1)/2; ans=max(ans,ret); } void dfs(int id,int pos) { if(id>k) { getans(); return; } for(int i=pos;i+(k-id)<=c[0];++i) { canmod[c[i]-'a']=true; dfs(id+1,i+1); canmod[c[i]-'a']=false; } } int main() { int T; scanf("%d",&T); while(T--) { ans=0; c[0]=0; memset(canmod,0,sizeof canmod); scanf("%d%d%s%s",&n,&k,a+1,b+1); for(int i=1;i<=n;++i) if(!canmod[a[i]-'a']) canmod[a[i]-'a']=true,c[++c[0]]=a[i]; if(c[0]<=k) getans(); else { memset(canmod,0,sizeof canmod); dfs(1,1); } printf("%lld\n",ans); } }
|
D. Flexible String Revisit
一个长度为$n$的01串,其中包含$k$个1,每次随机指定串中一个元素翻转,求整个01串第一次变为全0串的期望步数。
期望题。令$f_i$表示从$i$个1的状态经过随机过程第一次达到全0状态的期望步数。显然有$f_0=0$。试求$f_i$,在有$i$个1的状态下进行一次随机,有$\frac{i}{n}$的概率反转一个1,达到$f_{i-1}$的状态,还有$\frac{n-i}{n}$的概率反转一个1,达到$f_{i+1}$的状态,于是有$f_i = \frac{i}{n}f_{i-1}+\frac{n-i}{n}f_{i+1}+1$.
接下来就到了概率问题最难的环节了:试图解这个特殊的方程。我们已经找到了一个初始条件$f_0=0$,则有$f_1=\frac{1}{n}f_0+\frac{n-1}{n}f_2+1=1+\frac{n-1}{n}f_2$,这里只涉及$f_1$与$f_2$,如果带入$f_2=\frac{2}{n}f_1+\frac{n-2}{n}f_3+1$中,方程又会拆解为$f_2$与$f_3$之间的关系……这证明在$f_i$和$f_{i+1}$之间可以建立关系。令$f_i=a_i+b_if_{i+1}$,则有:
$$
\begin{align}
f_i &= \frac{i}{n} f_{i-1} + \frac{n-i}{n} f_{i+1} + 1 \\
&= \frac{i}{n} (a_{i-1}+b_{i-1}f_{i}) + \frac{n-i}{n} f_{i+1} + 1
\end{align}
$$
解得
$$
f_i = \frac{i a_{i-1}+n}{n-ib_{i-1}} + \frac{n-i}{n-ib_{i-1}} f_{i+1} \\
a_i = \frac{i a_{i-1}+n}{n-ib_{i-1}} \\
b_i = \frac{n-i}{n-ib_{i-1}} \\
(a_1 = 1, b_1 = \frac{n-1}{n})
$$
$a_i$与$b_i$均可递推得出确定值,于是我们就得到了$f_i$与$f_{i+1}$的递推关系。
我们再看另一侧,$f_n=f_{n-1}+1, f_{n-1}=\frac{n-1}{n}f_{n-2}+\frac{1}{n}f_{n}$,这里也可以用同样的方法进行计算:令$f_i=c_i+d_if_{i-1}$,则有:
$$
\begin{align}
f_i &= \frac{i}{n} f_{i-1} + \frac{n-i}{n} f_{i+1} + 1 \\
&= \frac{i}{n} f_{i-1} + \frac{n-i}{n} (c_{i+1} + d_{i+1}f_i) + 1
\end{align}
$$
解得:
$$
f_i = \frac{(n-i)c_{i+1}+n}{n-(n-i)d_{i+1}} + \frac{i}{n-(n-i)d_{i+1}} f_{i-1} \\
c_i = \frac{(n-i)c_{i+1}+n}{n-(n-i)d_{i+1}} \\
d_i = \frac{i}{n-(n-i)d_{i+1}} \\
(c_n = 1, d_n = 1)
$$
这样,我们得到了另一个$f_i$的递推式,其中$c_i$与$d_i$均可递推得出确定值。
联立两个$d_i$递推式可得:
$$
\begin{cases}
f_i = c_i + d_i f_{i-1} \\
f_{i-1} = a_{i-1} + b_{i-1} f_{i}
\end{cases} \\
\begin{align}
\Rightarrow f_i &= c_i + d_i a_{i-1} + b_{i-1} f_i \\
f_i &= \frac{c_i+d_ia_{i-1}}{1-d_ib_{i-1}}
\end{align}
$$
递推得到$a_i,b_i,c_i,d_i$,令$i=k$即可得到答案。
有两点需要注意:
- 需要特判$k=0$的情况。
- 分数的表达题目要求使用乘法逆元。$p$为素数,直接用快速幂法即可。
时间复杂度$O(n \log M)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include <cstdio> #include <algorithm> using namespace std; #define MAXN 1000010 #define MO 998244353ll int n,k; long long a[MAXN],b[MAXN],c[MAXN],d[MAXN]; char sa[MAXN],sb[MAXN]; long long inv(int x) { long long y=MO-2,tmp=x,ret=1; while(y) { if(y&1) ret=(ret*tmp)%MO; tmp=(tmp*tmp)%MO,y>>=1; } return ret; } long long sdiv(long long x,long long y) { x%=MO,y%=MO; if(x<0) x+=MO; if(y<0) y+=MO; return (x*inv(y))%MO; } int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%s%s",&n,sa+1,sb+1); k=0; for(int i=1;i<=n;++i) k+=sa[i]!=sb[i]; if(!k) { printf("0\n"); continue; } a[1]=1,b[1]=sdiv(n-1,n); for(int i=2;i<=n;++i) { a[i]=sdiv(i*a[i-1]+n,n-i*b[i-1]); b[i]=sdiv(n-i,n-i*b[i-1]); } c[n]=1,d[n]=1; for(int i=n-1;i;--i) { c[i]=sdiv((n-i)*c[i+1]+n,n-(n-i)*d[i+1]); d[i]=sdiv(i,n-(n-i)*d[i+1]); } printf("%lld\n",sdiv(c[k]+a[k-1]*d[k],1-b[k-1]*d[k])); } return 0; }
|
E. The Tree Has Fallen!
给你一颗$N$个点的树及其点权,$Q$次询问以某个点$r$为根的情况下,$v$的子树中子集最大异或和是多少。
$\sum n,q \le 2 \cdot 10^5$
树形 DP + 线性基 + DFS序 + 倍增算法 = 大融合怪。
首先考虑子集最大异或和问题。存储线性基,只需对每个数进行二进制拆分并实时高斯消元,$W$个数即可表达$0 \sim 2^W$范围内的整数的异或和情况。欲求子集最大异或和,对线性基中存储的元素进行异或即可。插入元素复杂度$O(W)$,两组线性基合并时间复杂度$O(W^2)$。以此为转移,使用树形 DP 即可获取全树所有子树的子集最大异或和。
按任意点为根定义有根树。当$r=v$时,输出根节点的结果;当$r$不在$v$子树内时,原定义子树结构与欲定义子树结构相同,输出以该点为根的子树的结果;当$r$在$v$子树内时,应输出除$v$往$r$方向向下第一个节点为根的子树之外所有节点为集合的结果:考虑 DFS 序,发现子树的 DFS 序是连续的,只需维护前缀和与后缀和线性基,合并即可。
这里线性基是子集最大异或和的标准做法,但 DFS 序比较难想。还有一个有趣的点,获取$v$往$r$方向向下第一个节点时,如果遍历儿子找点的话最坏时间复杂度$O(q n^2)$会 TLE(CF 数据你出的好啊),使用倍增算法维护祖先,跳着找即可。
时间复杂度$O(q \log^2 d)$,其中$d \le 10^9$为$a_i$取值范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| #include <cstdio> #include <algorithm> using namespace std; #define MAXN 200010 class hamel { private: int v[30]; public: hamel() { for(int i=0;i<30;++i) this->v[i]=0; } void clear() { for(int i=0;i<30;++i) this->v[i]=0; } void merge(hamel &b) { for(int i=0;i<30;++i) if(b.v[i]) this->insert(b.v[i]); } void insert(int x) { int to=-1; for(int i=29;~i;--i) if(x&(1<<i)) { if(v[i]) x^=v[i]; else if(to==-1) to=i; } if(to!=-1) { v[to]=x; for(int i=to+1;i<30;++i) if(v[i]&(1<<to)) v[i]^=x; } } int value() { int ret=0; for(int i=29;~i;--i) ret^=v[i]; return ret; } }; int n,a[MAXN]; hamel f[MAXN],g[MAXN],h[MAXN]; int to[MAXN<<1],nxt[MAXN<<1],head[MAXN],tot; int dfsi[MAXN],dfso[MAXN],dfsu[MAXN],cnt; int fa[MAXN][20],dep[MAXN]; void ae(int x,int y) { to[++tot]=y,nxt[tot]=head[x],head[x]=tot; } void dfs1(int pos,int pre) { dep[pos]=dep[pre]+1; fa[pos][0]=pre; dfsu[++cnt]=pos; dfsi[pos]=cnt; f[pos].insert(a[pos]); for(int i=head[pos];i;i=nxt[i]) if(to[i]!=pre) { dfs1(to[i],pos); f[pos].merge(f[to[i]]); } dfso[pos]=cnt; } int getson(int r,int v) { int i=0; while((1<<i)<=n) ++i; for(--i;~i;--i) if(dep[fa[r][i]]>dep[v]) r=fa[r][i]; return r; } int main() { int T,Q,x,y,r,v; scanf("%d",&T); while(T--) { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",a+i); for(int i=1;i<n;++i) scanf("%d%d",&x,&y),ae(x,y),ae(y,x); dfs1(1,0); for(int j=1;(1<<j)<=n;++j) for(int i=1;i<=n;++i) fa[i][j]=fa[fa[i][j-1]][j-1]; for(int i=1;i<=n;++i) { if(i>1) g[i].merge(g[i-1]); g[i].insert(a[dfsu[i]]); } for(int i=n;i;--i) { if(i<n) h[i].merge(h[i+1]); h[i].insert(a[dfsu[i]]); } scanf("%d",&Q); while(Q--) { scanf("%d%d",&r,&v); if(r==v) { printf("%d\n",f[1].value()); } else if(dfsi[v]<=dfsi[r]&&dfsi[r]<=dfso[v]) { int c=getson(r,v); hamel tmp; if(dfsi[c]-1>=1) tmp.merge(g[dfsi[c]-1]); if(dfso[c]+1<=n) tmp.merge(h[dfso[c]+1]); printf("%d\n",tmp.value()); } else printf("%d\n",f[v].value()); } for(int i=1;i<=n;++i) head[i]=0,f[i].clear(),g[i].clear(),h[i].clear(); cnt=tot=0; } return 0; }
|
F. Maximizing Root
输入一棵点数为$n$的有根树及其点权(点权$a_i \le 1000$)。你可以最多执行$k$次操作,每次操作你可以选择一个之前未选择过的点,并将其子树中所有点点权都乘以一个数$x$,其中$x$需为该子树中所有点权的公因子。求操作后根节点最大可能点权。
树形 DP 好题。令$f_{x,y}$为节点$x$及其子树令其包含公因子$y$(在该节点使用操作或不使用均可)的最少步数。思考如何递推:若已知所有儿子的所有情况,仅需枚举在该点进行操作的乘法因子$z$,其中$z$需满足:$z$是$a_x$的因子;$z \cdot a_x$是$y$的倍数。此时对所有儿子对应的子树,为了能够一起乘以$z$,需要都包含公因子$z$;为了乘以$z$之后都包含公因子$y$,需要都包含公因子$y/\gcd (y,z)$,取交集,其共需包含$\operatorname{lcm}(y,z)$,取$f_{u,y} = \max\limits_z(\sum f_{v,\operatorname{lcm}(y,z)}+1)$即可;当$z=1$,即最后操作乘以 1 时,实际上意味着在子树根节点不需要进行操作,此时特判最后不 +1 即可。初始情况,当$\gcd_x$($x$子树点权的最大公约数)是$y$的倍数时,$f_{x,y}=0$;当$\gcd_x \cdot \gcd_x$不是$y$的倍数时,$f_{x,y}$不合法。
使用记忆化搜索的方法进行递推。需要注意的是,我们求得的结果仅仅是可以乘$y$的最少步数,并不知道最后的乘数,而在最终求解时,根节点乘以多少是我们需要知道的答案,故将最后一层拆开,并不求$f_{1,y}$,而是求 1 号节点最后的乘数可以是多少。
时间复杂度$O(nm^2)$,其中$m \le \sqrt{max(a_i)} = \sqrt{1000}$为$a_1$因子个数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| #include <cstdio> #include <algorithm> #include <vector> #include <unordered_set> using namespace std; #define MAXN 100010 int n,k,a[MAXN],fa[MAXN],gcd[MAXN]; int f[MAXN][1001]; vector<int> to[MAXN]; vector<int> m[1010]; int lcm(int x,int y) { return x*y/__gcd(x,y); } void dfs(int pos,int pre) { fa[pos]=pre; gcd[pos]=a[pos]; for(auto i:to[pos]) if(i!=pre) { dfs(i,pos); gcd[pos]=__gcd(gcd[i],gcd[pos]); } } int solve(int x,int y) { if(f[x][y]!=-1) return f[x][y]; if(gcd[x]%y==0) return f[x][y]=0; if((a[x]*a[x])%y!=0) return f[x][y]=0x3f3f3f3f; f[x][y]=0x3f3f3f3f; for(int z:m[a[x]]) { if((a[x]*z)%y!=0) continue; int res=0,tmp; for(int i:to[x]) if(i!=fa[x]) { tmp=solve(i,lcm((y/__gcd(y,z)),z)); if(tmp==0x3f3f3f3f) { res=0x3f3f3f3f; break; } res+=tmp; } if(z==1) f[x][y]=min(f[x][y], res); else f[x][y]=min(f[x][y], res+1); } return f[x][y]; } int main() { for(int i=1;i<=1000;++i) { for(int j=1;j*j<=i;++j) if(i%j==0) { m[i].push_back(j); if(j!=i/j) m[i].push_back(i/j); } } int T,x,y; scanf("%d",&T); while(T--) { scanf("%d%d",&n,&k); for(int i=1;i<=n;++i) for(int j=1;j<=1000;++j) f[i][j]=-1; for(int i=1;i<=n;++i) to[i].clear(); for(int i=1;i<=n;++i) scanf("%d",a+i); for(int i=1;i<n;++i) scanf("%d%d",&x,&y),to[x].push_back(y), to[y].push_back(x); dfs(1,0); int ans=a[1]; for(auto i:m[a[1]]) { int res=0; for(auto j:to[1]) { solve(j,i); if(f[j][i]==0x3f3f3f3f) { res=-1; break; } res+=f[j][i]; } if(res!=-1&&(i==1&&res<=k||i!=1&&res<k)) ans=max(ans, i*a[1]); } printf("%d\n",ans); } return 0; }
|