Codeforces Global Round 14 题解(连续段dp)
E. Phoenix and Computers
题目描述
\(\tt zxy\) 点亮长度为 \(n\) 的序列,如果一个位置两边都被点亮那么这个位置自动点亮,\(\tt zxy\) 不能再次点亮一个已经亮的点,问有多少个不同的操作序列(也就是 \(\tt zxy\) 手动点亮的灯泡或者顺序不同),答案模质数 \(m\)
\(3\leq n\leq 400,10^8\leq m\leq 10^9\)
前言
不知道有没有人和我一样定义状态的,设 \(dp[i]...\) 表示处理前 \(i\) 个位置的方案数 \(...\) 然后发现怎么设计状态都做不动,可能这种做法就是不行吧。
解法1
既然本题又有手动点亮的灯泡,又有自动亮的灯泡,有点麻烦。不妨考虑简化的问题,如果一个长度为 \(x\) 的序列全部灯泡都由手动点亮有多少种方案数?可以枚举第一个点亮的灯泡 \(i\),不难发现 \((i,x]\) 这些灯泡只能按从小到大顺序点亮,\([1,i)\) 这些灯泡也只能按从大到小顺序点亮,两个序列都具有内部顺序,要求合并到一起的方案数。发现就是在 \(x-1\) 的数组中先填 \(i-1\) 个数:
这个子问题的结论能不能帮助我们呢?考虑最终的答案实际上是若干个手动点亮的序列拼接起来,然后中间是自动点亮的序列。若干个手动点亮的序列的拼合相当于重新标号的问题,所以可以用 \(\tt EGF\) 卷积来实现,设 \(F(x)\) 为手动点亮方案数的 \(\tt EGF\):
那么枚举手动点亮的序列个数 \(i\),可以知道自动点亮的灯泡个数为 \(i-1\),那么手动点亮的灯泡数为 \(n-i+1\),所以可以写出答案:
暴力实现卷积,时间复杂度 \(O(n^3)\),可以用任意模数 \(\tt NTT\) 优化到 \(O(n^2\log n)\)
#include <cstdio>
const int M = 405;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M],b[M],c[M],fac[M],inv[M];
void init()
{
a[1]=fac[0]=inv[0]=inv[1]=1;
for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%m;
for(int i=2;i<=n;i++) inv[i]=inv[m%i]*(m-m/i)%m;
for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%m;
for(int i=2;i<=n;i++) a[i]=a[i-1]*2%m;
for(int i=1;i<=n;i++) a[i]=a[i]*inv[i]%m;
}
void mul()
{
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++)
if(i+j<=n) c[i+j]=(c[i+j]+a[i]*b[j])%m;
for(int i=0;i<=n;i++)
b[i]=c[i],c[i]=0;
}
signed main()
{
n=read();m=read();
init();b[0]=1;
for(int i=1;i<=(n+1)/2;i++)
{
mul();
ans=(ans+fac[n-i+1]*b[n-i+1])%m;
}
printf("%lld\n",ans);
}
解法2(by jzm)
这个方法其实更重要,因为很多题都用得到他,听说过很久很久了,但是不会。
考虑连续段 \(dp\),设 \(dp[i][j]\) 表示 \(j\) 段长度总和是 \(i\) 的方案数,每两段之间任意长(但是长度\(\geq 2\))可以写出转移:
-
连接两个连续段,这两个连续段长度为 \(2\),有 \(j-1\) 组,每组两种选择:\(dp[i+2][j-1]\leftarrow dp[i][j]\times(j-1)\times 2\)
-
连接两个连续段,这两个连续段长度为 \(3\),有 \(j-1\) 组,每组一种选择:\(dp[i+3][j-1]\leftarrow dp[i][j]\times(j-1)\)
-
在某个连续段边上接一个数,连续段个数不变:\(dp[i+1][j]\leftarrow dp[i][j]\times 2j\)
-
在某个连续段边上隔一个位置接一个数,连续段个数不变:\(dp[i+2][j]\leftarrow dp[i][j]\times 2j\)
-
新开一个连续段,可以插入到任意两个连续段中:\(dp[i+1][j+1]\leftarrow dp[i][j]\times(j+1)\)
最后答案显然是 \(dp[n][1]\),时间复杂度 \(O(n^2)\)
趁着这个机会好好讲一下连续段 \(dp\),关键的问题是为什么我们能认为两个连续段之间是任意长的?
连续段 \(dp\) 的本质其实是逆向思维,之所以我们这么说是因为我们连续段之间是独立的,而对于连续段的合并其实就相当于原序列的拆分,这个拆分生成了互不相关的子问题,只是一般的 \(dp\) 是从原序列到子问题,连续段 \(dp\) 却是从子问题到原序列,正向是难以进行的,但是反过来是很好做的。
那么连续段 \(dp\) 能解决哪些问题呢?如果是涉及到关于一段连续区间的限制的序列计数问题就可以考虑用它了!
代码是嫖的
#include <bits/stdc++.h>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
int M; // module
inline void add(int&x,const int &y){
((x += y) >= M) ? (x -= M) : 0;
}
const int MaxN = 404;
int dp[MaxN][MaxN]; // total length, cnt
int main(){
int n = readint(); M = readint();
dp[1][1] = 1;
rep(i,1,n) rep(j,1,i){
add(dp[i+2][j-1],dp[i][j]*2ll*(j-1)%M); // j-1 gap, 2 choice
add(dp[i+3][j-1],dp[i][j]*(j-1ll)%M); // j-1 gap, 1 choice
add(dp[i+1][j],dp[i][j]*2ll*j%M); // 2*j endpos
add(dp[i+2][j],dp[i][j]*2ll*j%M); // one step from 2*j endpos
add(dp[i+1][j+1],dp[i][j]*(j+1ll)%M); // j+1 gap
}
printf("%d\n",dp[n][1]);
return 0;
}
F. Phoenix and Earthquake
题目描述
有 \(n\) 个点 \(m\) 条边的无向图,保证图联通,初始时所有的边都被破坏了,如果要重新修建两点之间的边需要 \(x\) 的花费,需要满足 \(a_u+a_v\geq x\) 才能修建边 \((u,v)\),每个城市初始有 \(a_i\) 元钱,如果两个城市联通可以实现资金共享,试构造方案使得图联通,如果不存在方案输出 NO
\(2\leq n,m\leq 300000\)
解法
贪心地看可以选取钱最多的连通块扩展它,你可能会觉得有点小问题,但他是对的。
首先考虑有无解的问题,如果所有城市的资金总和大于等于 \((n-1)\cdot x\) 那么就有解,否则无解。考虑每次选取钱最多的连通块,那么他和任意一个相连的城市都可以连边,因为如果不能的话剩下点资金的总和就大于 \((n-2)\cdot x\),但由于他们单个点的资金数一定小于 \(x\),所以导出矛盾,那么说明在任意时刻都是可以连边的。
可以直接选钱最多的连通块去模拟,用启发式合并可以维护每个点所连的边,时间复杂度 \(O(n\log n)\)
更优美的方法是直接 \(\tt dfs\),首先考虑叶子 \(v\) 和它的父亲 \(u\),如果 \(a_u+a_v\geq x\) 可以暴力连边,否则 \(a_v<x\),那么可以暂时不管 \(v\),剩下的点一定能构成一个大连通块,那么最后再把他接上去就行了,非叶子也类似,整个过程就是递归 \(-\) 回溯的过程,时间复杂度 \(O(n)\)
#include <cstdio>
const int M = 300005;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,x,fr,bk,res,tot,a[M],f[M],vis[M],ans[M];
struct edge
{
int v,next;
}e[2*M];
void dfs(int u)
{
vis[u]=1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(vis[v]) continue;
dfs(v);
if(a[u]+a[v]>=x)
{
a[u]+=a[v]-x;
ans[++fr]=(i+1)/2;
}
else
ans[--bk]=(i+1)/2;
}
}
signed main()
{
n=read();m=read();x=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
res+=a[i];
}
if(res<(n-1)*x)
{
puts("NO");
return 0;
}
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
e[++tot]=edge{v,f[u]},f[u]=tot;
e[++tot]=edge{u,f[v]},f[v]=tot;
}
bk=n;
dfs(1);
puts("YES");
for(int i=1;i<n;i++)
printf("%d\n",ans[i]);
}
H. Phoenix and Bits
题目描述
解法
首先注意读题哈,这道题是修改某个值域区间里面的值,我对着错误的题面想了好久
然后考虑用什么数据结构去维护,这道题的询问也是和值域有关的,所以要拿一个维护值域的数据结构来做。权值线段树不是很行,但是 \(\tt tire\) 数正好和二进制操作很符合。
先考虑第三个操作怎么做吧,因为异或体现在 \(\tt trie\) 树上就是交换若干个子树,可以用打标记的方式实现。我们把操作拆成 \(\tt log\) 个区间即可,如果只有第三个操作那么我们也可以轻松回答询问。
那么现在考虑怎么做或,如果是对整个 \(\tt trie\) 树做或你会不会?设或的值是 \(k\),那么如果 \(k\) 的某一位是 \(1\) 相当于把左子树合并到右子树上,而且合并是很能均摊分析的,就类比线段树合并我们来考虑这个问题。
其实合并的复杂度应该是对的,因为会花 \(O(1)\) 的时间减少一个节点。时间消耗主要是在找这些合并的东西上,如果左子树为空或者右子树为空那么根本没有合并的必要。现在的问题递归会消耗很多时间,那么我们思考什么时候才需要递归下去,就是这个子树内不存在左子树和右子树均非空,可以维护两个值 \(v[x],vk[x]\) 表示子树内存在值得并集和值取反之后的并集,如果 \(k\and vk[x]\and v[x]=0\) 就说明没有递归下去的必要,那么我们直接把这个子树异或上 \(k\and vk[x]\) 即可,也就是子树内的值都缺这些东西,把他用异或的方式或上去。
按照上面的方法我们再来分析一波复杂度,对于一个点合并的复杂度是 \(O(1)\),但是访问到他,让他合并的复杂度是 \(O(\log n)\),所以这部分的复杂度是 \(O(\)点数\(\times\log n)\) 的,多么高妙的均摊方法!
那么现在只需要解决值域区间 \([l,r]\) 的限制了!你发现直接分解成 \(\log n\) 个区间肯定会有问题,因为我们要从最高位开始访问才行。又有一个绝妙的方法,我们把值域在 \([l,r]\) 的 \(\tt trie\) 树分离下来,就类似于平衡树的 \(\tt split\) 操作,分裂成一个单独的 \(\tt trie\)
然后考虑并操作怎么做,实际上我们可以用 \(\or,\oplus\) 来表示 \(\and\),也就是我们先把 \(\tt trie\) 上面的所有值取反,然后或上 \(k\) 的取反,最后再把 \(\tt trie\) 上的值取反即可,考察所有操作发现节点数是 \(O(n\log c)\),所以时间复杂度 \(O(n\log ^2c)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 10000005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,k,q,rt,cnt,now,ls[M],rs[M],res[M],la[M],v[M],vk[M];
void up(int x)
{
v[x]=v[ls[x]]|v[rs[x]];
vk[x]=vk[ls[x]]|vk[rs[x]];
res[x]=res[ls[x]]+res[rs[x]];
}
void ins(int d,int &x,int val)
{
if(!x) x=++cnt;
if(d==0)
{
v[x]=val;vk[x]=val^k;res[x]=1;
return ;
}
if(val&(1<<(d-1))) ins(d-1,rs[x],val);
else ins(d-1,ls[x],val);
up(x);
}
void getxor(int d,int x,int val)
{
if(x==0) return ;
if(val&(1<<(d-1))) swap(ls[x],rs[x]);
int a=v[x],b=vk[x];
v[x]=(a&(k^val))|(b&val);
vk[x]=(b&(k^val))|(a&val);
la[x]^=val;
}
void down(int d,int x)
{
if(la[x]==0) return ;
getxor(d-1,ls[x],la[x]);getxor(d-1,rs[x],la[x]);
la[x]=0;
}
void split(int d,int &x,int &y,int l,int r,int L,int R)
{
if(x==0 || R<l || r<L) {y=0;return ;}
if(L<=l && r<=R) {y=x;x=0;return ;}
int mid=(l+r)>>1;y=++cnt;
down(d,x);
split(d-1,ls[x],ls[y],l,mid,L,R);
split(d-1,rs[x],rs[y],mid+1,r,L,R);
up(x);up(y);
}
void merge(int d,int &x,int &y)
{
if(x==0) {x=y;return ;}//这里好容易打错啊
if(y==0 || d==0) return ;//d==0的时候如果x,y都存在是不用合并的
down(d,x);down(d,y);
merge(d-1,ls[x],ls[y]);
merge(d-1,rs[x],rs[y]);
up(x);
}
void getor(int d,int x,int val)
{
if(!x) return ;
int ad=val&vk[x];
if((ad&v[x])==0) {getxor(d,x,ad);return ;}
down(d,x);
if(val&(1<<(d-1)))
{
getxor(d-1,ls[x],1<<(d-1));
merge(d-1,rs[x],ls[x]);
ls[x]=0;
}
getor(d-1,ls[x],val);
getor(d-1,rs[x],val);
up(x);
}
int ask(int d,int &x,int l,int r,int L,int R)
{
if(!x || L>r || l>R) return 0;
if(L<=l && r<=R) return res[x];
down(d,x);
int mid=(l+r)>>1;
return ask(d-1,ls[x],l,mid,L,R)+ask(d-1,rs[x],mid+1,r,L,R);
}
signed main()
{
n=read();q=read();k=(1<<20)-1;
for(int i=1;i<=n;i++)
{
int x=read();
ins(20,rt,x);
}
while(q--)
{
int now=0,t=read(),x=read(),y=read();
if(t==4)
{
printf("%d\n",ask(20,rt,0,k,x,y));
continue;
}
split(20,rt,now,0,k,x,y);
int z=read();
if(t==1)
getxor(20,now,k),getor(20,now,k^z),getxor(20,now,k);
if(t==2)
getor(20,now,z);
if(t==3)
getxor(20,now,z);
merge(20,rt,now);
}
}