Day 1
T1 预处理器
solution
按照题意模拟即可。使用map记下所有串对应的串以及当前正在被展开的串,然后暴力递归替换即可。
code
#include<bits/stdc++.h>
using namespace std;
map<string,string>mp;
map<string,bool>pd;
int n;string s;
inline bool ok(const char&x)
{
if('A'<=x&&x<='Z')return true;
if('a'<=x&&x<='z')return true;
return false;
}
inline int fd(int pos,const string&now)
{
while(pos<now.size()-1&&ok(now[pos+1]))++pos;
return pos;
}
string work(const string&now)
{
bool fl=0;
for(int i=0;i<now.size();++i)if(!ok(now[i])){fl=1;break;}
if(fl)
{
string res;
for(int l=0,r;l<now.size();l=r+1)
{
while(l<now.size()&&!ok(now[l]))
res.push_back(now[l++]);
if(l>=now.size())break;
r=fd(l,now);
string tmp;for(int i=l;i<=r;++i)tmp.push_back(now[i]);
res+=work(tmp);
}
return res;
}
if(!mp.count(now))return now;
string tmp=mp[now];
if(pd.count(now))return now;
pd[now]=1;
string res=work(tmp);
pd.erase(now);
return res;
}
int main()
{
cin>>n;getline(cin,s);
while(n--)
{
getline(cin,s);
int m=s.size();
if(s[0]=='#')
{
if(s[1]=='d')
{
int l=8,r=fd(l,s);
string t1,t2;
for(int i=l;i<=r;++i)t1.push_back(s[i]);
for(int i=r+2;i<s.size();++i)t2.push_back(s[i]);
mp[t1]=t2;
}
else
{
int l=7,r=fd(l,s);
string t1;
for(int i=l;i<=r;++i)t1.push_back(s[i]);
mp.erase(t1);
}
puts("");continue;
}
cout<<work(s)<<endl;
}
return 0;
}
T2 填树
description
给出一棵 \(n\) 个点的树,每个点 \(i\) 可以填一个数,但是其必须在 \([l_i,r_i]\) 以内。现在,你可以选择任意一条简单路径并将路径上的每个点都填上一个数。我们称一种填法是合法的当且仅当极差不超过给定值 \(K\) 。询问有多少种合法的填法以及所有填法中所有填的数之和是多少。
\(n\le 200,1\le l_i\le r_i\le 10^9,K\le 10^9\)
solution
一个切入点是假如我们已经确定了要填的是哪条路径后该怎么做。容易想到枚举最小值为 \(mn\) 然后计算填的数都在 \([mn,mn+K]\) 内的方案数。不过这样做会使得极差为 \(d\) 的方案被计算 \(K-d+1\) 次。为了补救,可以用 \(K\) 的答案减去 \(K-1\) 的答案,这样极差为 \(d\) 的方案就只会计算 \((K-d+1)-(K-1-d+1)=1\) 次,从而避免了重复。
现在的问题在于 \(mn\) 范围太大不好枚举。观察下面我们要计算的柿子:
考虑将柿子中的 \(\min,\max\) 拆开。可以把整个范围划分为 \(\mathcal O(n)\) 个区间满足对于其中任意一个区间 \([lp,rp]\) ,只要 \(mn\in[lp,rp]\) ,那么 \(mn,mn+K\) 和任意的 \(l_i,r_i\) 的大小关系都是确定的,从而拆开了 \(\min,\max\) 。此时的柿子是关于 \(mn\) 的至多 \(n\) 次多项式,我们需要知道 \(mn\) 取遍 \([lp,rp]\) 时该多项式的值之和。注意到多项式的前缀和也是多项式,于是可以插值插出该多项式的前缀和多项式,从而可以求出答案。
对于所有方案中的数之和,仍然观察柿子:
容易发现这个的做法和前面的做法是类似的。
然而现在的做法复杂度过高,无法通过。考虑优化。枚举树上路径所花费的时间太浪费了,可以先枚举 \(mn\) 所在的区间,然后用换根的树形dp统计到每个点的路径对应的多项式之和。不过由于我们最终要插值,因此只用记下点值之和即可。此时的复杂度便优化为 \(\mathcal O(n^3)\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N=205,mod=1e9+7,iv2=(mod+1)/2;
inline int add(int x,int y){return x+y>=mod?x+y-mod:x+y;}
inline int dec(int x,int y){return x-y<0?x-y+mod:x-y;}
inline void inc(int&x,int y){x=x+y>=mod?x+y-mod:x+y;}
inline void rec(int&x,int y){x=x-y<0?x-y+mod:x-y;}
inline int qpow(int x,int y)
{
int res=1;
for(;y;y>>=1,x=1ll*x*x%mod)
(y&1)&&(res=1ll*res*x%mod);
return res;
}
int n,K,l[N],r[N],tn[N<<2],tot;vector<int>e[N];
bool ban[N];int val1[N][N],val2[N][N],A[N][N],B[N][N],ifac[N],tmp1[N],tmp2[N];
void dfs1(int u,int f,int lp,int rp)
{
for(int v:e[u])if(v^f)dfs1(v,u,lp,rp);
int ls[2]={0,0},rs[2]={0,0};
if(!ban[u])
{
if(l[u]>=rp)ls[0]=l[u];
else ls[1]=1;
if(r[u]<=lp+K)rs[0]=r[u];
else rs[1]=1,rs[0]=K;
}
else rs[0]=mod-1;
int kp[2],sp[2];
for(int o:{0,1})
kp[o]=dec(rs[o],ls[o]),sp[o]=add(ls[o],rs[o]);
inc(kp[0],1);
for(int i=0;i<=n+2;++i)
{
A[u][i]=add(sp[0],1ll*sp[1]*i%mod);
B[u][i]=add(kp[0],1ll*kp[1]*i%mod);
val1[u][i]=B[u][i],val2[u][i]=0;
}
for(int v:e[u])if(v^f)
for(int i=0;i<=n+2;++i)
{
inc(val1[u][i],1ll*val1[v][i]*B[u][i]%mod);
inc(val2[u][i],1ll*val2[v][i]*B[u][i]%mod);
}
for(int i=0;i<=n+2;++i)
inc(val2[u][i],1ll*val1[u][i]*A[u][i]%mod);
}
void dfs2(int u,int f)
{
for(int v:e[u])if(v^f)
for(int i=0;i<=n+2;++i)
{
int now1=dec(val1[u][i],1ll*val1[v][i]*B[u][i]%mod);
int now2=dec(val2[u][i],add(1ll*val2[v][i]*B[u][i]%mod,1ll*val1[v][i]*B[u][i]%mod*A[u][i]%mod));
inc(val1[v][i],1ll*now1*B[v][i]%mod);
inc(val2[v][i],add(1ll*now2*B[v][i]%mod,1ll*now1*B[v][i]%mod*A[v][i]%mod));
}
for(int v:e[u])if(v^f)dfs2(v,u);
}
int pre[N],suf[N];
inline int calc(int*f,int x)
{
if(x<0)x+=mod;
if(x<=n+2)return f[x];
pre[0]=1;for(int i=1;i<=n+3;++i)pre[i]=1ll*pre[i-1]*(x-i+1)%mod;
suf[n+4]=1;for(int i=n+3;i;--i)suf[i]=1ll*suf[i+1]*(x-i+1)%mod;
int ans=0;
for(int i=0;i<=n+2;++i)
{
int now=1ll*f[i]*pre[i]%mod*suf[i+2]%mod*ifac[i]%mod*ifac[n+2-i]%mod;
if((n+2-i)&1)rec(ans,now);
else inc(ans,now);
}
return ans;
}
int ans1,ans2;
inline void work()
{
tot=0;ans1=ans2=0;
for(int i=1;i<=n;++i)
tn[++tot]=l[i]-K,tn[++tot]=l[i],tn[++tot]=r[i]-K,tn[++tot]=r[i]+1;
sort(tn+1,tn+tot+1);
tot=unique(tn+1,tn+tot+1)-tn-1;
for(int i=1;i<tot;++i)
{
int lp=tn[i],rp=tn[i+1]-1;
for(int u=1;u<=n;++u)
ban[u]=rp+K<l[u]||lp>r[u];
dfs1(1,0,lp,rp);
dfs2(1,0);
for(int t=0;t<=n+2;++t)tmp1[t]=tmp2[t]=0;
for(int u=1;u<=n;++u)if(!ban[u])
{
for(int t=1;t<=n+2;++t)
{
inc(val1[u][t],val1[u][t-1]);
inc(val2[u][t],val2[u][t-1]);
}
for(int t=0;t<=n+2;++t)
inc(tmp1[t],val1[u][t]),inc(tmp2[t],val2[u][t]);
}
inc(ans1,dec(calc(tmp1,rp),calc(tmp1,lp-1)));
inc(ans2,dec(calc(tmp2,rp),calc(tmp2,lp-1)));
}
}
int r1,r2;
int main()
{
scanf("%d%d",&n,&K);
for(int i=1;i<=n;++i)scanf("%d%d",l+i,r+i);
int fac=1;for(int i=1;i<=n+2;++i)fac=1ll*fac*i%mod;
ifac[n+2]=qpow(fac,mod-2);
for(int i=n+1;~i;--i)ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
for(int i=1,u,v;i<n;++i)
scanf("%d%d",&u,&v),e[u].push_back(v),e[v].push_back(u);
work();inc(r1,ans1),inc(r2,ans2);
--K;
work();rec(r1,ans1),rec(r2,ans2);
for(int i=1;i<=n;++i)
{
inc(r1,r[i]-l[i]+1);
inc(r2,1ll*(r[i]-l[i]+1)*(l[i]+r[i])%mod);
}
r1=1ll*r1*iv2%mod;
r2=1ll*r2*iv2%mod;
r2=1ll*r2*iv2%mod;
printf("%d\n%d\n",r1,r2);
return 0;
}
T3 学术社区
description
给出 \(m\) 条消息,第 \(i\) 条消息属于集合 \(s_i\in[1,n]\) 。其中某些消息还有两个特殊权值: \(t\in\{0,1\},d\in[1,n]\) ,如果 \(t=0\) 表示将该消息放置在属于集合 \(d\) 的消息后会带来 \(1\) 的贡献(称该类消息为楼下消息),如果 \(t=1\) 则表示将该消息放在属于集合 \(d\) 的消息前会带来 \(1\) 的贡献(称该类消息为楼上消息)。保证对于任意一个集合,其中存在某条消息满足其没有特殊权值(称该类消息为学术消息)。现在要将这 \(m\) 条消息排成一个排列使得贡献最大。要求构造方案。
多组数据, \(n\le m\le 77777,\sum m\le 2\times 10^5\) 。
solution
题目容易使人想到匹配。将 \(m\) 条消息拆为入点和出点,然后 \(u\) 的出点连接 \(v\) 的入点当且仅当如果 \(v\) 放在 \(u\) 后面会使得答案加一。此时跑二分图最大匹配就能得到答案。(直接跑过不了,需要优化建图)
不过手玩可以发现无法通过样例。为什么?注意到如果存在消息 \(a,b\) 满足:
- \(a\) 放在 \(b\) 前会贡献 \(1\) 。
- \(b\) 放在 \(a\) 后会贡献 \(1\) 。
我们称这样的 \((a,b)\) 是双向奔赴的。对于双向奔赴的消息 \((a,b)\) ,我们上面的做法无法统计到 \(a\) 的贡献。
于是先忽略这种情况,即特殊性质C,然后考虑求出答案后如何构造。构造的难点在于若存在消息组 \(a_1,a_2,\cdots,a_k\) ,满足 \(a_{i+1}\) 要在 \(a_{i}\) 后面, \(a_1\) 在 \(a_k\) 后面,即形成一个环。观察到这样的环有如下性质:
- 环中所有元素都不是学术消息,并且都是楼上消息或都是楼下消息。
不妨假设这个环的都是楼下消息。考虑和 \(a_1\) 在同一集合的学术消息 \(u\) ,假设 \(u\) 后面接 \(v\) ,那么可以将该环插入到 \(u,v\) 之间,即,将 \(u\to v\) 变为 \(u\to a_2\to a_3\to a_4\to\cdots\to a_k\to a_1\to v\) 。此时如果 \(v\) 会带来贡献,那么其一定是楼下消息,而 \(u,a_1\) 来自同一集合,因此如是构造后依然会有贡献。对于全是楼上消息的环做法类似。
那如果有双向奔赴的消息该怎么办呢?这里我们断言:如果 \(a,b\) 双向奔赴,那么将 \(a,b\) 放在一起一定不劣。证明考虑若存在某种方式 \(ax\cdots yb\) 的贡献严格优于 \(ab\cdots yx\) ,那么 \(ax,yb\) 至少要贡献 \(3\) ,那么 \(ax,yb\) 中至少有一对是双向奔赴的。不妨假设 \(ax\) 双向奔赴,那么 \(x,b\) 两条消息就是完全相同的,和“严格优于”相矛盾,因此贪心地将双向奔赴的消息匹配是没有问题的。
总结下,先将能双向奔赴的消息匹配,然后剩下的按照上述做法匹配即可。注意如果 \(a,b\) 双向奔赴了,那么要删去 \(a\) 的出点和 \(b\) 的入点,保留 \(a\) 的入点和 \(b\) 的出点。构造方案时需要借助学术消息将环断开。
单次复杂度为 \(\mathcal O(m\sqrt m)\) ,证明和二分图类似。
code
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5,M=4e6+5,inf=0x3f3f3f3f;
vector<int>w[N];int ct;
namespace Flow
{
int tot=1,S,T,n,fi[N],ne[M],to[M],c[M],d[N],cur[N];
inline void add(int x,int y,int z)
{
ne[++tot]=fi[x],fi[x]=tot,to[tot]=y,c[tot]=z;
ne[++tot]=fi[y],fi[y]=tot,to[tot]=x,c[tot]=0;
}
inline bool bfs()
{
fill(d+1,d+n+1,-1);d[S]=0;
queue<int>q;q.push(S);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=fi[u];i;i=ne[i])
{
int v=to[i];
if(c[i]&&d[v]==-1)
d[v]=d[u]+1,q.push(v);
}
}
return d[T]!=-1;
}
int dfs(int u,int flow)
{
if(u==T||flow==0)return flow;
int used=0;
for(int&i=cur[u];i;i=ne[i])
{
int v=to[i];
if(c[i]==0||d[v]!=d[u]+1)continue;
int t=dfs(v,min(flow-used,c[i]));
c[i]-=t,c[i^1]+=t;used+=t;
if(used==flow)break;
}
if(used<flow)d[u]=-1;
return used;
}
inline int dinic()
{
int e=0;
while(bfs())
{
memcpy(cur,fi,sizeof(int)*(n+1));
e+=dfs(S,inf);
}
return e;
}
inline void clear(){fill(fi+1,fi+n+1,0);tot=1;}
bool getw(int u)
{
w[ct].push_back(u);
if(u==T)return true;
for(int&i=fi[u];i;i=ne[i])
{
int v=to[i];
if((i&1)||!c[i^1])continue;
if(getw(v))
{
++c[i],--c[i^1];
return true;
}
}
w[ct].pop_back();
return false;
}
}
using Flow::add;
using Flow::S;
using Flow::T;
const string A="louxia",B="loushang";
using ll=long long;
int kase,n,m,ans,wo[N],dt[N],tp[N];bool ok[N];
unordered_map<string,int>id;
unordered_map<ll,int>ip1,ip2;
vector<int>v1[N],v2[N];int t1,t2;
int to[N];bool vis[N],in[N];
vector<int>cir[N][2];
inline void clear()
{
Flow::clear();
fill(ok+1,ok+m+1,0);
fill(wo+1,wo+m+1,0);
fill(dt+1,dt+m+1,0);
fill(tp+1,tp+m+1,0);
fill(to+1,to+m+1,0);
fill(in+1,in+m+1,0);
fill(vis+1,vis+m+1,0);
id.clear();
ip1.clear(),ip2.clear();
for(int i=1;i<=t1;++i)v1[i].clear();
for(int i=1;i<=t2;++i)v2[i].clear();
t1=t2=0;ans=0;
for(int i=1;i<=ct;++i)w[i].clear();
ct=0;
}
inline void go(int u)
{
int id=wo[u];
for(int v:cir[id][1])
{
cout<<v<<" ";int vv=v;
while(to[v]!=vv)v=to[v],cout<<v<<" ";
}
cir[id][1].clear();
cout<<u<<" ";
for(int v:cir[id][0])
{
int vv=v;
while(to[v]!=vv)v=to[v],cout<<v<<" ";
cout<<vv<<" ";
}
cir[id][0].clear();
}
inline void work()
{
cin>>n>>m;
for(int i=1;i<=n;++i)
{
string tmp;cin>>tmp;
id[tmp]=i;
}
for(int i=1;i<=m;++i)
{
string s1,s2,s3;cin>>s1>>s2>>s3;
int u=id[s1];tp[i]=0;wo[i]=u;
if(id.count(s2))
{
int v=id[s2];dt[i]=v;
if(s3==A)
{
ll S=(ll)v<<31|u;bool fl=0;tp[i]=1;
if(ip1.count(S))
{
int now=ip1[S];
if(!v1[now].empty())
{
ok[v1[now].back()]=ok[i]=1;
to[v1[now].back()]=i;
v1[now].pop_back();fl=1;ans+=2;
}
}
if(!fl)
{
S=(ll)u<<31|v;
int now=ip2.count(S)?ip2[S]:ip2[S]=++t2;
v2[now].push_back(i);
}
}
else if(s3==B)
{
ll S=(ll)v<<31|u;bool fl=0;tp[i]=2;
if(ip2.count(S))
{
int now=ip2[S];
if(!v2[now].empty())
{
ok[v2[now].back()]=ok[i]=1;
to[i]=v2[now].back();
v2[now].pop_back();fl=1;ans+=2;
}
}
if(!fl)
{
S=(ll)u<<31|v;
int now=ip1.count(S)?ip1[S]:ip1[S]=++t1;
v1[now].push_back(i);
}
}
}
}
S=2*n+2*m+1,T=S+1;Flow::n=T;
for(int i=1;i<=m;++i)
if(!ok[i])
{
if(tp[i]==1)add(dt[i]+m+m,i+m,1);
else if(tp[i]==2)add(i,dt[i]+m+m+n,1);
add(i,wo[i]+m+m,1);add(S,i,1);
add(wo[i]+m+m+n,i+m,1);add(i+m,T,1);
}
else
{
if(tp[i]==1)add(i,wo[i]+m+m,1),add(S,i,1);
else if(tp[i]==2)add(wo[i]+m+m+n,i+m,1),add(i+m,T,1);
}
cout<<ans+Flow::dinic()<<endl;
while(1)
{
++ct;
if(!Flow::getw(S)){--ct;break;}
}
for(int i=1;i<=ct;++i)to[w[i][1]]=w[i][3]-m;
for(int i=1;i<=m;++i)if(to[i])in[to[i]]=1;
for(int i=1;i<=m;++i)if(!in[i])
{
int u=i;vis[u]=1;
while(to[u])u=to[u],vis[u]=1;
}
for(int i=1;i<=m;++i)if(!vis[i])
{
cir[wo[i]][tp[i]-1].push_back(i);
int u=i;vis[u]=1;
while(!vis[to[u]])u=to[u],vis[u]=1;
}
for(int i=1;i<=m;++i)if(!in[i])
{
int u=i;
while(u)
{
if(tp[u]==0)go(u);
else cout<<u<<" ";
u=to[u];
}
}
cout<<endl;
clear();
}
int main()
{
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin>>kase;
while(kase-->0)work();
return 0;
}
Day 2
T1 卡牌
description
给出 \(n\) 个数,第 \(i\) 个数为 \(s_i\) 。有 \(m\) 次询问,第 \(i\) 次询问给出 \(c_i\) 个质数,然后问在 \(n\) 个数中有多少种选取的方法满足选出来的数的乘积能被每个给出的质数整除。
\(n\le 10^6,s_i\le 2000,m\le 1500,\sum_ic_i\le 18000\)
solution
最朴素的想法是状压,即令 \(f_{i,S}\) 表示考虑了值为 \(1\sim i\) 的所有数,包含的质因子集合为 \(S\) 时的方案数。然而 \(2000\) 以内的质数多达 \(303\) 个,复杂度太高。
但是我们观察到这样一条性质:一个正整数的所有质因子中大于其算术平方根的最多有一个。于是我们将所有数按照其大于根号的质因数进行分组,在每一组中对小于根号的质因数状压求解(小于根号的质因数仅有14个)。如果当前询问包含该大于根号的质因数,那么就钦定在该组中至少选取一个。对于组与组之间的合并,设 \(g_S,f_S\) 分别表示总答案和组内答案,那么有转移:
这是经典的或卷积,可以使用FMT进行优化。
然而倘若每次询问都这样做,那么仍然无法承受。我们首先预处理出每组无限制/至少选取一个对应的FMT数组,然后先默认每组都无限制乘起来得到一个“标准数组”。询问时先拿到“标准数组”,若遇到某个大于根号的质数,那么就先除以无限制的FMT数组然后乘上至少选取一个的FMT数组,最后再iFMT回来即可求解。
询问总复杂度为 \(\mathcal O(2^{\pi(\sqrt n)}(\sum_ic_i+\pi(\sqrt n)m))\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N=2005,mod=998244353,B=42,lim=2000;
inline int add(int x,int y){return x+y>=mod?x+y-mod:x+y;}
inline void inc(int&x,int y){x=x+y>=mod?x+y-mod:x+y;}
inline int dec(int x,int y){return x-y<0?x-y+mod:x-y;}
inline void rec(int&x,int y){x=x-y<0?x-y+mod:x-y;}
inline int qpow(int x,int y)
{
if(x==1)return 1;
int res=1;
for(;y;y>>=1,x=1ll*x*x%mod)
(y&1)&&(res=1ll*res*x%mod);
return res;
}
int n,m,c,p[N],ct[N],id[N],seq[N],f[1<<13],g[1<<13],pw[1000005],st,rp[500][1<<13],kp[500][1<<13],gp[1<<13],go[N];
inline void FWT(int*f,int tp)
{
if(count(f,f+st,0)==st)return;
for(int mid=1,len=2;len<=st;len<<=1,mid<<=1)
for(int i=0;i+len-1<st;i+=len)
for(int k=0;k<mid;++k)
tp==1?inc(f[i+mid+k],f[i+k]):rec(f[i+mid+k],f[i+k]);
}
int pr[N],pcnt;bool flag[N];
inline void sieve()
{
flag[1]=1;
for(int i=2;i<=lim;++i)
{
if(!flag[i])pr[++pcnt]=i;
for(int j=1;j<=pcnt;++j)
{
int num=i*pr[j];if(num>lim)break;
flag[num]=1;if(i%pr[j]==0)break;
}
}
}
vector<int>prt;
inline void prework()
{
for(int i=pcnt;i;--i)
{
if(pr[i]<=B)break;
for(int j=pr[i];j<=lim;j+=pr[i])id[j]=pr[i];
}
for(int i=1;i<=pcnt;++i)
if(pr[i]<=B)prt.push_back(pr[i]);
else break;
for(int i=1;i<=lim;++i)seq[i]=i;
sort(seq+1,seq+lim+1,[&](const int&x,const int&y){return id[x]<id[y];});
st=1<<prt.size();int co=0;
fill(gp,gp+st,1);
for(int l=1,r;l<=lim;l=r+1)
{
r=l;while(r<lim&&id[seq[r+1]]==id[seq[l]])++r;
++co;go[l]=r+1;f[0]=1;
for(int i=l;i<=r;++i)
{
int u=seq[i];
if(ct[u])
{
int O=0;
for(int t=0;t<prt.size();++t)
if(u%prt[t]==0)O|=1<<t;
int val=dec(pw[ct[u]],1);
for(int S=st-1;~S;--S)if(f[S])
f[S|O]=(f[S|O]+1ll*val*f[S])%mod;
}
}
memcpy(rp[co],f,sizeof f);FWT(rp[co],1);
if(id[seq[l]]>0)rec(f[0],1);
memcpy(kp[co],f,sizeof f);FWT(kp[co],1);
for(int i=0;i<st;++i)
kp[co][i]=1ll*kp[co][i]*qpow(rp[co][i],mod-2)%mod,
gp[i]=1ll*gp[i]*rp[co][i]%mod;
fill(f,f+st,0);
}
}
bool pd[N];
inline void work()
{
scanf("%d",&c);
for(int i=1;i<=c;++i)scanf("%d",p+i);
sort(p+1,p+c+1);
c=unique(p+1,p+c+1)-p-1;
for(int i=1;i<=c;++i)pd[p[i]]=1;
memcpy(g,gp,sizeof gp);
for(int i=1,co=1;i<=lim;i=go[i],++co)if(pd[id[seq[i]]])
for(int t=0;t<st;++t)g[t]=1ll*g[t]*kp[co][t]%mod;
FWT(g,-1);int O=0,ans=0;
for(int i=0;i<prt.size();++i)
if(pd[prt[i]])O|=1<<i;
for(int i=0;i<st;++i)
if((O&i)==O)inc(ans,g[i]);
printf("%d\n",ans);
for(int i=1;i<=c;++i)pd[p[i]]=0;
}
int main()
{
scanf("%d",&n);
pw[0]=1;for(int i=1;i<=n;++i)pw[i]=add(pw[i-1],pw[i-1]);
for(int i=1,d;i<=n;++i)scanf("%d",&d),++ct[d];
sieve();prework();scanf("%d",&m);
while(m--)work();
return 0;
}
T2 序列变换
description
给出 \(x,y\) 和一个长度为 \(2n\) 的合法括号序列,每次你可以进行如下两种操作之一:
- 将形如 \(\texttt{p(A)(B)q}\) 变为 \(\texttt{p(A()B)q}\) ,花费的代价为 \(x\) 乘 \(\texttt{(A)}\) 中第一个左括号的权值加上 \(y\) 乘 \(\texttt{(B)}\) 中第一个左括号的权值。
- 将形如 \(\texttt{pABq}\) 变为 \(\texttt{pBAq}\) ,不需要代价。
其中 \(\texttt{A,B}\) 都是合法的括号序列, \(\texttt{p,q}\) 可以是任意括号序列。
现在要求将给出序列变成 \(\texttt{(((((((((...)))))))))}\) 的形式所需要的最小花费。
\(x,y\in\{0,1\},n\le 4\times 10^5\)
solution
梦回WC2022,于是考虑将括号序列转成树。即遇到左括号就向下访问,遇到右括号就回溯,形成树形结构。
那么题目中的操作1对应于对于两个有相同父亲且相邻的子树 \(\texttt{A,B}\) ,先把 \(\texttt B\) 这个点单独挂到 \(\texttt A\) 下面,再将 \(\texttt{B}\) 的所有儿子依次挂到 \(\texttt{A}\) 下面。而操作2则对应于交换相邻的子树,这意味着我们不用要求操作1对于相邻的要求。最后我们要求将这棵树变成一条链,于是考虑按照深度从上至下依次做,每层都保留一个节点,然后将其他的全部下放到下一层。不妨设当前层有 \(sz\) 个节点,所有节点的代价和是 \(sum\) 。
分情况讨论:
-
\(x=y=0\) :答案就是0。
-
\(x=0,y=1\) :每次操作的代价是被下方的节点的代价。每一层的代价是 \(sum-\) 当前层留下的节点代价。那么贪心地选择代价最大的留下就是最优的。
-
\(x=y=1\) :每次操作的代价是操作的两个节点的代价之和。对于当前层,不妨设代价最小的为 \(mn\) 。如果保留的节点就是 \(mn\) ,那么直接将其他节点挂到 \(mn\) 下,代价为 \((sz-2)mn+sum\) ;否则,可以先将其他节点挂到 \(mn\) 下,最后再将 \(mn\) 挂到要保留的节点下,代价仍然为 \((sz-2)mn+sum\) 。不管选择那个点保留,当前层付出的代价都是相同的。但是为了在后面的层中最小化 \(mn,sum\) ,应该每次选择最大值保留在当前层。
-
\(x=1,y=0\) :每次操作的代价是被保留的节点代价。对于当前层,不妨设要保留的点代价为 \(w\) ,最小代价为 \(mn\) ,那么当前层代价就是 \((sz-2)mn+w\) 。我们先忽略初始时 \(sz=1\) 的层,然后将这个代价分开来看。对于前半部分,我们希望一直保持将最小值下传;对于后半部分,求和后答案是总和减去链尾的点代价,于是希望最大值也能被下传。如果 \(sz>2\) ,则两个条件能同时满足,直接将最值都下传即可。如果 \(sz=2\) 且不是倒数第二层(倒数第二层 \(sz=2\) 的情况是平凡的),那么需要重新考虑。观察 \(sz\) 随着深度的变化: \(sz_{dep+1}=sz_{dep}-1+s\) ,其中 \(s\) 表示当前层所有节点的儿子个数和。发现如果某时刻 \(s=0\) ,那么之后都有 \(s=0\) ,那么 \(sz\) 的变化就是先单调不减,最后单调每次 \(-1\) 。这意味着非平凡的 \(sz=2\) 的层一定组成一个前缀。这里我们断言:最终下放的节点权值一定是这些层中所有节点的最大值/最小值。
考虑证明这一点。假如最终下传的节点代价为 \(w\) ,且不是最值。相比于 \(mn\) 而言,其最多节省 \(w-mn\) 的代价,而如果 \(w\) 在某一刻成为了最小值,那么会多付出 \((sz-2)(w-mn)\) 的代价,这显然是不优于直接下方最小值的;而如果 \(w\) 从未成为最小值,那么不如选择最大值 \(mx\) 下传。
使用set模拟上述过程。复杂度 \(\mathcal O(n\log n)\) 。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
using ll=long long;ll ans;
int n,x,y,val[N];char s[N];
vector<int>vp[N];
namespace sub1
{
multiset<int>s;
inline int main()
{
ll sum=0;
for(int dep=1;;++dep)
{
if(vp[dep].empty()&&s.empty())break;
for(int u:vp[dep])s.insert(u),sum+=u;
int now=*s.rbegin();
sum-=now;s.erase(s.find(now));
ans+=sum;
}
printf("%lld\n",ans);
return 0;
}
}
namespace sub2
{
multiset<int>s;
inline int main()
{
ll sum=0;
for(int dep=1;;++dep)
{
// cout<<dep<<" "<<sum<<endl;
if(vp[dep].empty()&&s.empty())break;
for(int u:vp[dep])s.insert(u),sum+=u;
if(s.size()>1)ans+=1ll*(*s.begin())*(s.size()-2)+sum;
int now=*s.rbegin();
sum-=now;s.erase(s.find(now));
}
printf("%lld\n",ans);
return 0;
}
}
namespace sub3
{
multiset<int>s;ll sum;
inline ll work(int dep,int x)
{
s.clear();
if(x>0)s.insert(x);ll res=0;
for(;;++dep)
{
if(vp[dep].empty())break;
for(int u:vp[dep])s.insert(u);
res+=1ll*(*s.begin())*(s.size()-2);
s.erase(next(s.begin()));
}
int t=s.size()-2;
if(t>=1)res+=1ll*t*(t+1)/2*(*s.begin());
res+=sum-(*s.rbegin());
return res;
}
inline int main()
{
sum=accumulate(val+1,val+n+1,0ll);
int dep=1;
while(vp[dep].size()==1)sum-=vp[dep++].back();
int lim=dep+1;
if(vp[dep].size()==2)while(vp[lim].size()==1)++lim;
else --lim;
if(lim==dep)return printf("%lld\n",work(lim,-1)),0;
vector<int>tmp;
for(int i=dep;i<lim;++i)
{
for(int u:vp[i])tmp.push_back(u);
sort(tmp.begin(),tmp.end());
tmp.pop_back();
}
ans=work(lim,tmp.back());
tmp.clear();
for(int i=dep;i<lim;++i)
{
for(int u:vp[i])tmp.push_back(u);
sort(tmp.begin(),tmp.end(),greater<int>());
tmp.pop_back();
}
ans=min(ans,work(lim,tmp.back()));
printf("%lld\n",ans);
return 0;
}
}
int main()
{
scanf("%d%d%d%s",&n,&x,&y,s+1);
for(int i=1;i<=n;++i)scanf("%d",val+i);
for(int i=1,dep=0,j=1;i<=2*n;++i)
{
dep+=s[i]=='('?1:-1;
if(s[i]=='(')vp[dep].push_back(val[j++]);
}
if(x==0&&y==0)return puts("0"),0;
if(x==0&&y==1)return sub1::main();
if(x==1&&y==1)return sub2::main();
if(x==1&&y==0)return sub3::main();
return 0;
}
T3 最大权独立集问题
description
给定 \(n\) 个点的点带权二叉树。删去一条边的代价是边两端节点的点权和。现在要求按照某种顺序删去所有边使得总代价最小。
\(n\le 5000\)
solution
对于一棵子树 \(u\) ,其难以处理的断边是 \(u\) 和 \(fa_u\) 之间的断边,因为这会将子树内的某个点权移出子树,同时将子树外的某个点权移入子树。
我们针对这一点设计 dp :令 \(f_{u,a,b}\) 表示考虑子树 \(u\) ,将点权移出去的点是 \(a\) ,移进来的点权最终到达的节点是\(b\) 时将子树内所有边全部断掉(要包含 \(u,fa_u\) 之间的边)所需要的最小代价(不考虑移进来点权带来的贡献)。容易发现 \(a,b\) 一定是在不同子树内,因此每个点对只会在 \(\texttt{lca}\) 处贡献一次,有用的状态就是 \(\mathcal O(n^2)\) 的。而观察到我们事实上只关心 \(b\) 距离 \(u\) 的距离 \(d\) ,因此将状态改进为 \(f_{u,a,d}\) 。
接下来考虑转移。不妨设左右儿子分别为 \(lc,rc\) ,枚举断边顺序,有如下讨论:
- 断边顺序为 \(fa\to lc\to rc\) :( \(fa\to rc\to lc\) 类似)
\(u\) 先被移出子树,接下来移进来的点权进入左子树,然后左子树中的某个点权进入右子树,同时右子树的某个点权到 \(u\) 上。于是我们有转移:
当确定了 \(v_1,d_1\) 以后,只需要最小化 \(h(v_1)=f_{rc,v_2,d_2}+c_{v_1}(d_2+1)\) 。而对于每个 \(h(v_1)\) ,我们枚举 \(d_2\) ,从而只需最小化 \(g(d_2)=f_{rc,v_2,d_2}\) 。先计算处所有 \(g(d_2)\) ,总复杂度为所有 \(f\) 的合法状态数;然后计算所有 \(h(v_1)\) ,总复杂度为 \(\sum sz_{lc}sz_{rc}\) ;最后枚举 \(v_1,d_1\) 计算 \(f_{u,u,d_1+1}\) ,总复杂度为所有 \(f\) 的合法状态数。可以发现上面的复杂度都是 \(\mathcal O(n^2)\) 的。
- 断边顺序为 \(lc\to fa\to rc\) :( \(rc\to fa\to lc\) 类似)
\(u\) 先被移入左子树,然后左子树中的某个点权被移出,之后移进来的点权被移入右子树,同时右子树的某个点权到 \(u\) 上。我们有转移:
做法和前面是类似的,不再赘述。
- 断边顺序为 \(lc\to rc\to fa\) :( \(rc\to lc\to fa\) 类似)
\(u\) 先被移入左子树,然后左子树中某个点权移入右子树,之后右子树的某个点权被移出,同时移进来的点权到 \(u\) 上。我们有转移:
做法仍然和前面是类似的,不再赘述。
总复杂度为 \(\mathcal O(n^2)\) 。注意对于没有左/右儿子,根的情况都要特殊判断,写起来有些繁琐。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5005;
using ll=long long;const ll inf=1e16;
int n,c[N],ls[N],rs[N],fa[N];
vector<int>son[N];
vector<ll>f[N][N];
inline void Min(ll&x,ll y){x>y?x=y:0;}
inline void upd(int u,int v,int d,ll val)
{
while(f[u][v].size()<=d)f[u][v].push_back(inf);
Min(f[u][v][d],val);
}
ll g[N],h[N];
inline void work(int u)
{
int lc=ls[u],rc=rs[u];
if(fa[u])
{
if(!lc&&!rc)upd(u,u,0,c[u]);
else if(!lc)
{
for(int v:son[rc])
for(int d=0;d<f[rc][v].size();++d)
{
upd(u,u,d+1,f[rc][v][d]+c[u]);
upd(u,v,0,f[rc][v][d]+1ll*c[u]*(d+1)+c[v]);
}
}
else if(!rc)
{
for(int v:son[lc])
for(int d=0;d<f[lc][v].size();++d)
{
upd(u,u,d+1,f[lc][v][d]+c[u]);
upd(u,v,0,f[lc][v][d]+1ll*c[u]*(d+1)+c[v]);
}
}
else
{
int md=0;
for(int v:son[rc])
{
for(int d=0;d<f[rc][v].size();++d)
Min(g[d],f[rc][v][d]);
md=max(md,(int)f[rc][v].size());
}
for(int v:son[lc])
for(int d=0;d<md;++d)
Min(h[v],g[d]+1ll*c[v]*(d+1));
for(int v:son[lc])
{
int ss=f[lc][v].size()+1,p=f[u][u].size();
if(p<ss)
{
f[u][u].resize(ss);
for(int o=p;o<ss;++o)f[u][u][o]=inf;
}
for(int d=0;d<f[lc][v].size();++d)
upd(u,u,d+1,f[lc][v][d]+h[v]+c[u]);
}
for(int v:son[lc])h[v]=inf;
for(int v:son[lc])
for(int d=0;d<f[lc][v].size();++d)
Min(h[v],f[lc][v][d]+1ll*c[u]*(d+1));
for(int v:son[lc])
{
int ss=md+1,p=f[u][v].size();
if(p<ss)
{
f[u][v].resize(ss);
for(int o=p;o<ss;++o)f[u][v][o]=inf;
}
for(int d=0;d<md;++d)
upd(u,v,d+1,h[v]+g[d]+c[v]);
}
for(int i=0;i<md;++i)g[i]=inf;
for(int v:son[lc])h[v]=inf;
for(int v:son[lc])
for(int d=0;d<f[lc][v].size();++d)
Min(g[v],f[lc][v][d]+1ll*c[u]*(d+1));
for(int v:son[lc])
for(int d=0;d<md;++d)
Min(h[d],g[v]+1ll*c[v]*(d+1));
for(int v:son[rc])
for(int d=0;d<f[rc][v].size();++d)
upd(u,v,0,h[d]+f[rc][v][d]+c[v]);
for(int v:son[lc])g[v]=inf;
for(int i=0;i<md;++i)h[i]=inf;md=0;
}
}
else
{
if(!lc&&!rc)upd(u,0,0,0);
else if(!lc)
{
for(int v:son[rc])
for(int d=0;d<f[rc][v].size();++d)
upd(u,0,0,f[rc][v][d]+1ll*c[u]*(d+1));
}
else if(!rc)
{
for(int v:son[lc])
for(int d=0;d<f[lc][v].size();++d)
upd(u,0,0,f[lc][v][d]+1ll*c[u]*(d+1));
}
else
{
int md=0;
for(int v:son[rc])
{
for(int d=0;d<f[rc][v].size();++d)
Min(g[d],f[rc][v][d]);
md=max(md,(int)f[rc][v].size());
}
for(int v:son[lc])
for(int d=0;d<md;++d)
Min(h[v],g[d]+1ll*c[v]*(d+1));
for(int v:son[lc])
for(int d=0;d<f[lc][v].size();++d)
upd(u,0,0,f[lc][v][d]+h[v]+1ll*c[u]*(d+1));
for(int i=0;i<md;++i)g[i]=inf;
for(int v:son[lc])h[v]=inf;
}
}
}
int sz[N];
void pdfs(int u)
{
sz[u]=1;
if(ls[u])pdfs(ls[u]),sz[u]+=sz[ls[u]];
if(rs[u])pdfs(rs[u]),sz[u]+=sz[rs[u]];
}
void dfs(int u)
{
int lc=ls[u],rc=rs[u],cc=0;
son[u].resize(sz[u]);
son[u][cc++]=u;
if(lc)
{
dfs(lc);
for(int v:son[lc])son[u][cc++]=v;
}
if(rc)
{
dfs(rc);
for(int v:son[rc])son[u][cc++]=v;
}
work(u);
if(ls[u]&&rs[u])swap(ls[u],rs[u]),work(u);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",c+i);
for(int i=2,f;i<=n;++i)
{
scanf("%d",&f);
if(!ls[f])ls[f]=i;
else rs[f]=i;
fa[i]=f;
}
pdfs(1);
for(int i=0;i<=n;++i)g[i]=h[i]=inf;
dfs(1);printf("%lld\n",f[1][0][0]);
return 0;
}