六校联考2019CSP 选做
更新中...
以下所有标注“片段”的参考代码,都缺少一个“头”,“头”里包括了一些我的习惯性的简写(如typedef long long ll;
)。没有“头”并不影响理解代码。如果想要完整代码,可以去本博客公告栏的链接里,把这个“头”复制到“代码片段”前面。
day1-流量
根据“每个点处流入的流量之和等于流出的流量之和”,当且仅当一个点仅有一条未知流量的边时,可以推断得知这条边的状态。当然,这种推断是会产生连锁反应的。例如,初始时,只有几个点满足“仅有一条未知流量的边”,然后我们把这些边推断出来后,又会产生一些新的点满足这一条件。
进一步,发现,如果还未确定状态的边(构成的子图)中存在一个环,则环上的边的流量,永远无法被推断出来。同时发现,若不存在环,则一定可以推断出所有边的流量(按上一段描述的过程,从叶子到根进行“连锁反应”即可)。因此,我们不询问的边的集合,就是原图的最大生成森林。
求这个最大生成森林即可。注意,\(w_i\leq 0\)的边一定要询问(因为问了没有坏处),也就是不被加入最大生成森林中。最后,设最大生成森林的边权和为\(s\),则答案就是\((\sum_{i=1}^{m}w_i)-s\)。
时间复杂度\(O(m\log m+n\log n)\)。
参考代码(片段):
const int MAXN=2e5,MAXM=5e5;
int n,m,fa[MAXN+5],sz[MAXN+5];
int get_fa(int x){return (x==fa[x])?x:(fa[x]=get_fa(fa[x]));}
struct ed{int u,v,w;}e[MAXM+5];
bool cmp(ed x,ed y){return x.w>y.w;}
int main() {
cin>>n>>m;ll sum=0;
for(int i=1;i<=n;++i)fa[i]=i,sz[i]=1;
for(int i=1;i<=m;++i){cin>>e[i].u>>e[i].v>>e[i].w;sum+=e[i].w;}
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;++i){
if(e[i].w<=0)break;
int u=get_fa(e[i].u),v=get_fa(e[i].v);
if(u!=v){
sum-=e[i].w;
if(sz[u]>sz[v])swap(u,v);
fa[u]=v;
sz[v]+=sz[u];
}
}
cout<<sum<<endl;
return 0;
}
day1-个人练习生
考虑二分答案\(\text{mid}\)。可以对每个点\(u\),求出一个\(\text{lim}[u]\)表示点\(u\)最迟什么时候必须出发。
先令\(\text{lim}[u]=\text{mid}-a[u]-\text{dep}[u]\)(\(\text{dep}[u]\)是点\(u\)到根路径上的边数)。但这还不完全,因为我们还要考虑到“祖先必须在后代之后出发”这一要求。于是再做一遍dfs,让每个点的\(\text{lim}[u]\),对\(\text{lim}[fa(u)]-1\)取\(\min\)。这样就求出了真正的\(\text{lim}[u]\):每个点的最迟出发时间。
然后可以做一个贪心:对\(\text{lim}[1\dots n]\)排序,让值小的先出发。发现当前答案\(\text{mid}\)合法,当且仅当排好序后,\(\forall i\in[1,n]:\text{lim}[i]\geq i-1\)。
因为要二分答案和排序,时间复杂度\(O(n\cdot \log n\cdot \log a)\),只能拿到\(80\)分。
发现其实不用二分答案。因为\(\text{mid}\)的大小,并不会影响\(\text{lim}[i]\)的相对大小关系。所以可以先假设\(\text{mid}=0\),求出\(\text{lim}[1\dots n]\)并排好序。然后答案就等于:\(\max_{i=1}^{n}\{i-1-\text{lim}[i]\}\)。
时间复杂度\(O(n\log n)\),\(\log\)来自排序。
参考代码(片段):
const int MAXN=3e5,MAXA=1e9;
int n,a[MAXN+5];
vector<int>G[MAXN+5];
int fa[MAXN+5],dep[MAXN+5];
void dfs_prep(int u){
for(int i=0;i<SZ(G[u]);++i){
int v=G[u][i];
if(v==fa[u]) continue;
fa[v]=u;
dep[v]=dep[u]+1;
dfs_prep(v);
}
}
int lim[MAXN+5];
void dfs_calc_lim(int u){
for(int i=0;i<SZ(G[u]);++i){
int v=G[u][i];
if(v==fa[u]) continue;
lim[v]=min(lim[v],lim[u]-1);
dfs_calc_lim(v);
}
}
int main() {
cin>>n;
for(int i=1;i<=n;++i)
cin>>a[i];
for(int i=1;i<n;++i){
int u,v; cin>>u>>v;
G[u].pb(v); G[v].pb(u);
}
dfs_prep(1);
for(int i=1;i<=n;++i)
lim[i]=-a[i]-dep[i];
dfs_calc_lim(1);
sort(lim+1,lim+n+1);
int ans=0;
for(int i=1;i<=n;++i){
ans=max(ans,i-1-lim[i]);
}
cout<<ans<<endl;
return 0;
}
day1-假摔
考虑一对可能成为最优方案的\((A,B)\),要满足什么条件。
- 如果存在一个\(x\),满足\(A<x<B\),且\(a_x\geq a_A\),则\((x,B,C)\)是一组更优的合法解。因此:\(\forall x\in(A,B):a_x<a_A\)。
- 如果存在一个\(x\),满足\(A<x<B\),且\(a_x\geq a_B\),则\((A,x,C)\)是一组更优的合法解,因此:\(\forall x\in (A,B):a_x<a_B\)。
因此,合法的\((A,B)\),总共只有\(O(n)\)对:也就是每个位置\(A\)和它后面第一个\(a_B\geq a_A\)的\(B\);以及每个位置\(B\),和它前面第一个\(a_A\geq a_B\)的\(A\)。可以用单调栈求出。
然后考虑回答询问。
离线。从大到小枚举左端点,每次加入一个\(A\)(也就是加入若干对\((A,B)\))。
对每个位置\(i\),假设我们维护出一个\(f[i]\)表示以\(i\)作为\(C\)时的最优答案。假设当前枚举到的左端点为\(l\),当前要处理的询问右端点为\(r\),则我们只需要查询\(f\)数组在\([l,r]\)区间内的最大值即可。
考虑加入一个\(A\)对\(f\)数组的影响。前面说过,这也就是加入以它为\(A\)的所有\((A,B)\)。因为\((A,B)\)总数只有\(O(n)\)对,所以可以暴力依次加入。对一对\((A,B)\)而言,它影响到的\(C\),必须满足\(B-A\leq C-B\),移项得\(C\geq 2B-A\)。因此我们对所有\(i\geq 2B-A\),令\(f[i]\)对\(a_A+a_B+a_i\)取\(\max\)即可。
用线段树维护。修改操作有些特殊。做法是,预处理出每个区间,初始时\(a_i\)的最大值,记为\(\text{maxa}(l,r)\)。这样区间修改时,令\(\text{maxf}(l,r):=\max(\text{maxf}(l,r),\text{maxa}(l,r)+a_A+a_B)\)即可。区间查询就是直接求\(\text{maxf}\)的最大值。
时间复杂度\(O((n+q)\log n)\)。
考虑这道题的本质。一开始是一个\((A,B,C)\)三元问题,很不好回答。我们是怎么拆解它的?用离线去掉了一元\(A\),通过观察\(A,B\)的关系去掉了一元\(B\),然后剩下的一元\(C\)用数据结构维护它。
参考代码(片段):
const int MAXN=5e5;
pii sta[MAXN+5];
int n,m,a[MAXN+5],top;
ll ans[MAXN+5];
struct Q{int l,r,id;}q[MAXN+5];
bool cmp(Q x,Q y){return x.l>y.l;}
struct SegmentTree{
//区间取max,求区间max
ll val[MAXN*4+5],tag[MAXN*4+5],mx[MAXN*4+5];
void build(int p,int l,int r){
if(l==r){val[p]=mx[p]=a[l];return;}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
val[p]=mx[p]=max(val[p<<1],val[p<<1|1]);
}
void push_down(int p){
if(tag[p]){
val[p<<1]=max(val[p<<1],mx[p<<1]+tag[p]);
val[p<<1|1]=max(val[p<<1|1],mx[p<<1|1]+tag[p]);
tag[p<<1]=max(tag[p<<1],tag[p]);
tag[p<<1|1]=max(tag[p<<1|1],tag[p]);
tag[p]=0;
}
}
void modify(int p,int l,int r,int ql,int qr,ll v){
if(ql<=l && qr>=r){val[p]=max(val[p],mx[p]+v);tag[p]=max(tag[p],v);return;}
push_down(p);
int mid=(l+r)>>1;
if(ql<=mid)modify(p<<1,l,mid,ql,qr,v);
if(qr>mid)modify(p<<1|1,mid+1,r,ql,qr,v);
val[p]=max(val[p<<1],val[p<<1|1]);
}
ll query(int p,int l,int r,int ql,int qr){
if(ql<=l && qr>=r)return val[p];
push_down(p);
int mid=(l+r)>>1;ll res=0;
if(ql<=mid)res=query(p<<1,l,mid,ql,qr);
if(qr>mid)res=max(res,query(p<<1|1,mid+1,r,ql,qr));
return res;
}
}T;
int main() {
cin>>n;
for(int i=1;i<=n;++i)cin>>a[i];
cin>>m;
for(int i=1;i<=m;++i){cin>>q[i].l>>q[i].r;q[i].id=i;}
sort(q+1,q+m+1,cmp);
T.build(1,1,n);
sta[top=1]=mk(a[n-1],n-1);
int j=n-2;
for(int i=1;i<=m;){
for(;j>=q[i].l;--j){
while(top && sta[top].fst<=a[j]){
if(sta[top].scd*2-j<=n)T.modify(1,1,n,sta[top].scd*2-j,n,a[j]+sta[top].fst);
--top;
}
if(top&&sta[top].scd*2-j<=n)T.modify(1,1,n,sta[top].scd*2-j,n,a[j]+sta[top].fst);
sta[++top]=mk(a[j],j);
}
int ii=i;
for(;ii<=m&&q[ii].l==q[i].l;++ii)ans[q[ii].id]=T.query(1,1,n,q[ii].l+2,q[ii].r);
i=ii;
}
for(int i=1;i<=m;++i)cout<<ans[i]<<endl;
return 0;
}
day2-文体两开花
对每个点,维护一个\(f(u)\)表示它所有儿子的\(val\)的异或和。再维护一个\(g(u)\),表示所有儿子的\(f\)的异或和。
这样,修改和查询都可以\(O(1)\)维护。
时间复杂度\(O(n+q)\)。
参考代码(片段):
const int MAXN=1e6,MOD=1e9+7;
int n,m,val[MAXN+5],fa[MAXN+5],f[MAXN+5],g[MAXN+5];
vector<int>G[MAXN+5];
void dfs(int u){
f[u]=val[u];
for(int i=0;i<(int)G[u].size();++i){
int v=G[u][i];
if(v==fa[u])continue;
fa[v]=u;
dfs(v);
f[u]^=val[v];
g[u]^=f[v];
}
}
int main() {
cin>>n>>m;
for(int i=1;i<=n;++i)cin>>val[i];
for(int i=1,u,v;i<n;++i){cin>>u>>v;G[u].pb(v),G[v].pb(u);}
dfs(1);f[0]=val[1];
int ans=0;
for(int i=1;i<=m;++i){
int u,x;cin>>u>>v;
if(fa[fa[u]])g[fa[fa[u]]]^=f[fa[u]];
g[fa[u]]^=val[u];g[fa[u]]^=x;
f[fa[u]]^=val[u];f[fa[u]]^=x;
f[u]^=val[u];f[u]^=x;
if(fa[fa[u]])g[fa[fa[u]]]^=f[fa[u]];
ans=(ans+(ll)(g[u]^f[fa[u]]^val[fa[fa[u]]])%MOD*i%MOD*i%MOD)%MOD;
val[u]=x;
}
cout<<ans<<endl;
return 0;
}
day2-国际影星
补集转化,求不合法的方案,也就是点\(1\)能到达的点与点\(2\)能到达的点交为空。
因为\(n\)很小。考虑状压DP。设\(dp_{1/2}[S]\),表示从点\(1\)或\(2\)出发,只考虑点集\(S\)及其内部的边的情况下,能到达\(S\)里任意一个点,此时给点集\(S\)内部的边定向的方案数。
更明确地讲,一个点集内部的边,就是指两个端点都在点集里的边。我们不难对每个点集,预处理出它内部的边的数量,记为\(E[S]\)。
转移时,还是考虑补集转化,用总的方案减去不合法的方案。总的方案就是\(2^{E[S]}\),即任意连边。考虑计算不合法的方案数。枚举一个真子集\(T\subsetneq S\),计算恰好只能到达点集\(T\)的方案数(显然\(1\)或\(2\)必须在\(T\)中)。此时\(S\setminus T\)内部可以任意连边,而\(T\)与\(S\setminus T\)之间的边必须都连向\(T\)。所以可以得到转移:
DP完成后,考虑统计答案。枚举两个互不相交的集合\(s_1,s_2\)表示最终\(1\)能到达的点集和\(2\)能到达的点集。设\(r\)表示其他点的集合,即\(r=\{1,\dots ,n\}\setminus s_1\setminus s_2\)。则\(r\)内部的的边可以任意连,而\(r\)与\(s_1,s_2\)之间的边必须全部连向\(s_1\)或\(s_2\)。\(s_1,s_2\)之间不能有连边。
主要涉及到子集枚举,用这个技巧:for(int i=s; i; i=((i-1)&s))
可以实现不重不漏的枚举。
时间复杂度\(O(3^n)\)。
参考代码(片段):
const int M=(1<<16),MOD=1e9+7;
inline int mod(int x){return x<MOD?(x<0?x+MOD:x):x-MOD;}
int n,m,G[16],pw[300],cnt[M],f[M],g[M];
int main() {
pw[0]=1;for(int i=1;i<300;++i)pw[i]=mod(pw[i-1]<<1);
n=read();m=read();
for(int i=1;i<=m;++i){int u=read()-1,v=read()-1;G[u]|=(1<<v);G[v]|=(1<<u);}
for(int s=1;s<(1<<n);++s){
for(int i=0;i<n;++i)if(s&(1<<i))cnt[s]+=__builtin_popcount(G[i]&s);
cnt[s]>>=1;
}
f[1]=1;
for(int s=5;s<(1<<n);s+=4){
f[s]=pw[cnt[s]];
int c=s^1;
for(int ss=(c-1)&s;;ss=(ss-1)&s){
f[s]=mod(f[s]-(ll)f[ss^1]*pw[cnt[c^ss]]%MOD);
if(!ss)break;
}
}
g[2]=1;
for(int s=6;s<(1<<n);s+=4){
g[s]=pw[cnt[s]];
int c=s^2;
for(int ss=(c-1)&s;;ss=(ss-1)&s){
g[s]=mod(g[s]-(ll)g[ss^2]*pw[cnt[c^ss]]%MOD);
if(!ss)break;
}
}
int ans=pw[m];
for(int s1=1;s1<(1<<n);s1+=4){
int c=s1;
for(int i=0;i<n;++i)if(s1&(1<<i))c|=G[i];
if(c&2)continue;
c=(((1<<n)-1)&(~c))^2;
for(int s2=c;;s2=(s2-1)&c){
ans=mod(ans-(ll)f[s1]*g[s2^2]%MOD*pw[cnt[((1<<n)-1)^s1^(s2^2)]]%MOD);
if(!s2)break;
}
}
cout<<ans<<endl;
return 0;
}
day2-零糖麦片
相当于有一个\(01\)序列,每次可以让连续的一段异或\(1\)。初始时有\(k\)个位置上是\(1\)。问最少多少次操作后,可以把整个序列变成\(0\)。
对原序列做\(\operatorname{xor}\)意义下的差分。具体来说,设原序列为\(a[0\dots n]\),并且钦定\(a[0]=0\)。那么差分序列就是\(b[1\dots n]\)满足\(b[i]=a[i]\operatorname{xor}a[i-1]\)。
那么“让一段区间异或\(1\)”,就变成了让差分序列上两个点异或\(1\)。并且原序列全是\(0\),就等价于差分序列全是\(0\)(充分必要)。
要把差分序列上这些\(1\)消去,考虑将它们两两匹配。对于一对匹配了的\(1\),要将它们消去:
- 如果他们距离为一个奇质数,则需要\(1\)次操作。
- 如果他们距离为一个偶数,则需要\(2\)次操作。这是根据哥德巴赫猜想:任何一个大于等于\(6\)的偶数一定能拆成两个奇质数的和,并且它被证明在\(10^7\)以内是正确的。
- 如果他们距离为一个非质数的奇数,则需要\(3\)次操作:拆成一个奇质数和一个偶数。
于是根据贪心,我们肯定希望距离为奇质数的匹配尽可能多。又发现这样的匹配,两个点一定一个是奇数一个是偶数,所以可以根据奇偶性建二分图,求出最大匹配。
剩下的点,尽量和同奇偶性的匹配即可。
时间复杂度\(O(\text{flow}(k,k^2))\)。
const int N=1e7,INF=1e9;
int n,a[1005],p[1000000],cntp,pos0[2005],pos1[2005],ct0,ct1;
bool v[N+1];
void sieve(){
v[1]=1;
for(int i=2;i<=N;++i){
if(!v[i])p[++cntp]=i;
for(int j=1;j<=cntp&&i*p[j]<=N;++j){
v[i*p[j]]=1;
if(i%p[j]==0)break;
}
}
}
namespace Flowinginging{
const int MAXN=2010;
struct EDGE{int nxt,to,w;}edge[MAXN*MAXN];
int head[MAXN],tot;
void add_edge(int u,int v,int w){
edge[++tot].nxt=head[u];edge[tot].to=v;edge[tot].w=w;head[u]=tot;
edge[++tot].nxt=head[v];edge[tot].to=u;edge[tot].w=0;head[v]=tot;
}
int d[MAXN],cur[MAXN];
bool bfs(int s,int t){
memset(d,0,sizeof(d));d[s]=1;
queue<int>q;q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(!d[v]&&edge[i].w){
d[v]=d[u]+1;
if(v==t)return 1;
q.push(v);
}
}
}
return 0;
}
int dfs(int u,int t,int flow){
if(u==t)return flow;
int rest=flow;
for(int &i=cur[u];i&&rest;i=edge[i].nxt){
int v=edge[i].to;
if(d[v]==d[u]+1 && edge[i].w){
int k=dfs(v,t,min(rest,edge[i].w));
if(!k)d[v]=0;
else{
edge[i].w-=k;
edge[i^1].w+=k;
rest-=k;
//return flow-rest;
}
}
}
return flow-rest;
}
int maxflow(int s,int t){
int ans=0,tmp;
while(bfs(s,t)){
for(int i=1;i<=t;++i)cur[i]=head[i];
while(tmp=dfs(s,t,INF))ans+=tmp;
}
return ans;
}
};
using namespace Flowinginging;
int main() {
sieve();
cin>>n;
for(int i=1;i<=n;++i)cin>>a[i];
sort(a+1,a+n+1);
for(int i=1;i<=n;){
int j=i+1;
while(j<=n && a[j]==a[j-1]+1)++j;
--j;
if(a[i]&1)pos1[++ct1]=a[i];else pos0[++ct0]=a[i];
if((a[j]+1)&1)pos1[++ct1]=a[j]+1;else pos0[++ct0]=a[j]+1;
i=j+1;
}
sort(pos0+1,pos0+ct0+1);
sort(pos1+1,pos1+ct1+1);
int s=ct0+ct1+1,t=s+1;
tot=1;
for(int i=1;i<=ct0;++i){
add_edge(s,i,1);
for(int j=1;j<=ct1;++j){
int d=abs(pos1[j]-pos0[i]);assert(d<=N);
if(!v[d])add_edge(i,ct0+j,1);
}
}
for(int i=1;i<=ct1;++i)add_edge(ct0+i,t,1);
int ans=maxflow(s,t);
ct0-=ans;ct1-=ans;
ans+=(ct0/2)*2+(ct1/2)*2;
if(ct0&1)ans+=3;
cout<<ans<<endl;
return 0;
}
day3-序列
考虑要使原序列前\(i\)个位置的和是\(m\)的倍数。发现,不管前\(i-1\)个位置怎么填,第\(i\)个位置都有且仅有唯一一种填法,使得和是\(m\)的倍数。同理,也会有恰好\(m-1\)种填法,使得和不是\(m\)的倍数。
于是问题就转化为,钦定\(j\) (\(j\geq k\))个位置前缀和是\(m\)的倍数,其他位置都不是。因此答案等于:
预处理阶乘和逆元,可以\(O(1)\)求组合数。总时间复杂度\(O(n)\)。
参考代码(片段):
const int MAXN=1e5,MOD=998244353;
inline int mod(int x){return x<MOD?x:x-MOD;}
inline int pow_mod(int x,int i){
int y=1;
while(i){
if(i&1)y=(ll)y*x%MOD;
x=(ll)x*x%MOD;
i>>=1;
}
return y;
}
int n,m,K,fac[MAXN+5],invf[MAXN+5];
inline int comb(int n,int k){
if(n<k)return 0;
return (ll)fac[n]*invf[k]%MOD*invf[n-k]%MOD;
}
int main() {
fac[0]=1;for(int i=1;i<=MAXN;++i)fac[i]=(ll)fac[i-1]*i%MOD;
invf[MAXN]=pow_mod(fac[MAXN],MOD-2);
for(int i=MAXN-1;i>=0;--i)invf[i]=(ll)invf[i+1]*(i+1)%MOD;
int T;cin>>T;while(T--){
cin>>n>>m>>K;
if(K>n){puts("0");continue;}
int t=pow_mod(m-1,n-K),iv=pow_mod(m-1,MOD-2),ans=0;
for(int i=K;i<=n;++i){
ans=mod(ans+(ll)comb(n,i)*t%MOD);
t=(ll)t*iv%MOD;
}
cout<<ans<<endl;
}
return 0;
}
day3-图
考虑给图上的点染色,使得在最终的答案路径中,\(k\)个点的颜色都各不相同。这样我们就可以状压DP解决。设\(dp[s][u]\)表示已经经过了\(s\)里的这些颜色,走到了点\(u\),经过的最长路径长度。转移时枚举下一步走到哪里。时间复杂度\(O(2^k(n+m))\)。
现在问题是我们还不知道这个染色方案。考虑随机染色:每个点的颜色在\(k\)种颜色里随机。
这样总共有\(k^n\)种染色方案。其中能使得答案路径上点颜色各不相同的染色方案有\(k!\cdot k^{n-k}\)。正确率是\(\frac{k!\cdot k^{n-k}}{k^n}=\frac{k!}{k^k}\)。
多次随机,可以通过本题。
参考代码(片段):
const int MAXN=3e4;
struct EDGE{int nxt,to,w;}edge[200005];
int n,m,K,head[MAXN+5],tot,ans,f[MAXN+5][1<<8],col[MAXN+5],M;
inline void add_edge(int u,int v,int w){edge[++tot].nxt=head[u];edge[tot].to=v;edge[tot].w=w;head[u]=tot;}
void solve(){
for(int i=1;i<=n;++i){
col[i]=rand()%K;
for(int j=0;j<M;++j)f[i][j]=-1;
f[i][1<<col[i]]=0;
}
for(int s=1;s<M-1;++s){
for(int u=1;u<=n;++u){
if(f[u][s]<0)continue;
for(int i=head[u];i;i=edge[i].nxt){
int v=edge[i].to;
if(!(s&(1<<col[v])) && f[v][s^(1<<col[v])]<f[u][s]+edge[i].w){
f[v][s^(1<<col[v])]=f[u][s]+edge[i].w;
}
}
}
}
for(int i=1;i<=n;++i)ans=max(ans,f[i][M-1]);
}
int main() {
srand((ull)"dysyn1314");
cin>>n>>m>>K;
for(int i=1,u,v,w;i<=m;++i){cin>>u>>v>>w;add_edge(u,v,w);add_edge(v,u,w);}
int beg=clock();M=1<<K;ans=-1;
while(1.0*(clock()-beg)/CLOCKS_PER_SEC<1.5)solve();
cout<<ans<<endl;
return 0;
}
day3-商店
简化一下问题。有三个序列\(a[1\dots n],b[1\dots n],c[1\dots n]\)。满足 \(\forall i: a[i]\geq b[i]\)。你要选出一个\(\{1,\dots,n\}\)的子集\(S\)。满足:
并且最大化:
朴素的DP:设\(dp[i][x][y]\)表示考虑了前\(i\)个物品,选出的物品\(a\)之和为\(x\),\(b\)之和为\(y\),能得到的最大的\(c\)之和。转移时考虑当前物品选或不选,是\(O(1)\)的。总时间复杂度\(O(np^2)\)。
发现这个DP没有用到一个重要的条件:\(\forall i:a[i]\geq b[i]\)。
考虑这样一个状态:\(dp[i][z]\),表示考虑了前\(i\)个物品,选出的物品\(a\)之和\(\geq z\),\(b\)之和\(\leq z\)。发现每个\(z-a[i]\leq z'\leq z-b[i]\),都可以转移到\(z\)。也就是说,对每个\(i\),我们枚举\(z\)之后,能转移到每个\(z\)的是一段长度相同的滑动窗口。用单调队列维护这段滑动窗口里\(dp[i-1][z']\)的最大值即可。
时间复杂度\(O(np)\)。空间可以用滚动数组优化到\(O(n+p)\)。
参考代码(片段):
int n,p,a[1005],b[1005],c[1005];
ll dp[2][100005];
int main() {
cin>>n>>p;
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=1;i<=n;++i)cin>>b[i];
for(int i=1;i<=n;++i)cin>>c[i];
memset(dp,0x3f,sizeof(dp));dp[0][0]=0;
for(int i=1,ti=1;i<=n;++i,ti^=1){
deque<int>dq;
for(int j=0;j<=p;++j){
while(!dq.empty()&&dq.front()<j-a[i])dq.pop_front();
if(j-b[i]>=0){
while(!dq.empty()&&dp[ti^1][dq.back()]>=dp[ti^1][j-b[i]])dq.pop_back();
dq.push_back(j-b[i]);
}
dp[ti][j]=dp[ti^1][j];
if(!dq.empty())dp[ti][j]=min(dp[ti][j],dp[ti^1][dq.front()]+c[i]);
}
}
cout<<dp[n&1][p]<<endl;
return 0;
}
day9-谦逊
引理1:任何一个数经过若干次谦逊操作,一定能得到一个小于等于\(9\)的数。并且,假设原数是\(x\),则得到的这个“小于等于\(9\)的数”就是\((x-1)\bmod 9+1\)。也就是说,对于$x=1,2,\dots $,最终得到的结果,每\(9\)个数为一周期,不断循环。
引理2:\(\forall k\),\(i\times k\bmod 9\)的值,对所有\(i=0,1,2,\dots\),每\(9\)个数为一周期,永远循环。
引理3:任何一个小于\(10^{16}\)的数,最多需要用\(3\)次谦逊操作,就能变成小于等于\(9\)的数。
以上三条引理都可以通过简单的找规律或逻辑分析得出。此处略去证明。
根据引理1可知,任何数使用谦逊的结果,只和它模\(9\)的余数有关。又根据引理2,对于一个\(k\),想要得到所有可能的模\(9\)余数,只需要使用不超过\(8\)次力量祝福。综上,我们最多使用\(8\)力量祝福,一定能得到最优答案。
容易想到的一种做法是,枚举使用力量祝福的次数\(i\) (\(0\leq i\leq 8\)),用\((\text{digitSum}(n+i\times k),i+\text{calc}(n+i\times k))\)更新答案。其中\(\text{digitSum}\)表示数位和,\(\text{calc}\)表示变成小于等于\(9\)的数,需要使用的谦逊操作数。
但上述的做法是不对的。虽然能求出最优的结果,却不一定能最小化操作次数。也就是说,有可能在中间某一步先搞一次谦逊操作,会比使用完所有力量祝福后再一起用谦逊更优。
那怎么办呢?如果你转而去想高妙的贪心,或挖掘更深的性质,就容易陷入自闭。此时需要用到引理3,也就是\(\text{calc}(n+i\times k)\)的值不超过\(3\)。所以总操作次数最多为\(11\)。我们可以爆搜所有可能的操作方案!按最坏情况粗略估计,大约要搜\(\sum_{x=1}^{11}2^x< 2^{12}\)种操作方案,实际远远达不到。可以轻松AC本题。
时间复杂度\(O(?)\)。
参考代码(片段):
ll sum_dig(ll x){
ll res=0;
while(x)res+=(x%10),x/=10;
return res;
}
ll N,K;
pii ans;
void dfs(int u,int v,ll n){
if(n<=9)
ckmin(ans, mk((int)n,u+v));
if(u+v==11)
return;
if(u<8){
dfs(u+1,v,n+K);
}
if(v<3){
dfs(u,v+1,sum_dig(n));
}
}
void solve_case(){
cin>>N>>K;
ans=mk(10,233);
dfs(0,0,N);
cout<<ans.fi<<" "<<ans.se<<endl;
}
int main() {
int T;cin>>T;while(T--){
solve_case();
}
return 0;
}
day9-排序
官方题解给出的构造方案(原文):
- 首先给每个格子钦点一个可接受的范围区间。
- 然后依次一个一个处理左上角格子的数,把它移到应该去的格子里。 如上图,如果左上角最上层的盘子是\(83\),我们就把它移到\((1,3)\)去, 然后处理下一个。
- 然后按从大区间到小区间的次序把所有区间移到右下角,使它们最后是排好序的。如上图,先把\([91−99]\)移到右下角,然后是\([81−90]\), 以此类推。
也就是说,我们把左上角\(6\times6\)的问题,转化成若干个小子问题。从大到小,依次递归解决每个子问题,最终就能把整个局面排好序了。
那我们现在就要问:一个\(n\times m\)的矩阵,从左上角进、右下角出,最多能把多少个盘子排好序。我们可以严谨地说明一下,“进”就是指左上角的格子原本是空的,然后我们突然放一堆盘子进来,它们是乱序的;“出”就是指把这些盘子,按下面大、上面小的顺序排好后,堆叠到右下角的格子上方(右下角的格子里可能原本已有一些盘子,不过不影响。我们放在它们上面即可)。具体到本题中,其实所有的“右下角”,都是指整个\(6\times6\)矩形的右下角,也就是坐标\((6,6)\)的这个格子;而左上角,则不确定只哪个格子,取决于我们讨论的\(n\times m\)的大小。
我们记,一个\(n\times m\)的矩阵,最多能把多少个盘子排好序,这个数量为\(f(n,m)\)。
首先,\(f(1,1)=1\)。
然后根据前面所说,我们解决这个问题的方法其实就是分治(把要排序的数分发下去)。所以除了左上角外的每个格子,都被分发到一段数。因此他们能排序的量的总和,就是\(n\times m\)能排序的数量。即:\(f(n,m)=\sum_{i=1}^{n}\sum_{j=1}^{m}[(i,j)\neq (n,m)]\times f(i,j)\)。
按此式子计算,得出\(f(6,6)=26928\)。不足以通过本题。
发现\(1\times 2\)或\(2\times 1\)的格子,通过手动构造,能排好\(2\)个盘子而非\(1\)个。所以强制令\(f(1,2)=f(2,1)=2\)后,可以算出\(f(6,6)=42960\),可以通过本题。
时间复杂度\(O(n)\)。
具体的递归实现,可以见代码。希望本题可以帮助读者更好的理解“分治”和“排序”这两个基础概念。
参考代码(片段):
const int MAXN=4e4;
int f[10][10];
int n;
struct Oper{
int r,c,num;
char d;
Oper(int _r,int _c,char _d,int _num){
r=_r; c=_c; d=_d; num=_num;
}
Oper(){}
};
vector<Oper>ans;
void move(int frm_r,int frm_c,int to_r,int to_c){
int r=frm_r, c=frm_c;
while(r<to_r){
ans.pb(Oper(r,c,'D',1));
++r;
}
while(c<to_c){
ans.pb(Oper(r,c,'R',1));
++c;
}
}
void solve(int pos_r,int pos_c,int val_l,int val_r,vector<int> a){
if(!SZ(a)) return;
if(pos_r==6 && pos_c==6) return;
if(pos_r==5 && pos_c==6){
assert(SZ(a)<=2);
if(SZ(a)==1) move(5,6,6,6);
else{
if(a[0]>a[1]) ans.pb(Oper(5,6,'D',2));
else{ move(5,6,6,6); move(5,6,6,6); }
}
return;
}
if(pos_r==6 && pos_c==5){
assert(SZ(a)<=2);
if(SZ(a)==1) move(6,5,6,6);
else{
if(a[0]>a[1]) ans.pb(Oper(6,5,'R',2));
else{ move(6,5,6,6); move(6,5,6,6); }
}
return;
}
vector<vector<pii> > range(7,vector<pii>(7));
vector<vector<vector<int> > > vec(7,vector<vector<int> >(7));
int cur=val_l;
for(int i=pos_r;i<=6;++i){
for(int j=pos_c+(i==pos_r);j<=6;++j){
int size=f[6-i+1][6-j+1];
range[i][j]=mk(cur,cur+size-1);
cur+=size;
}
}
assert(cur-1>=val_r);
for(int k=SZ(a)-1;k>=0;--k){
int x=a[k];
assert(val_l<=x && val_r>=x);
bool flag=0;
for(int i=pos_r;i<=6;++i){
for(int j=pos_c+(i==pos_r);j<=6;++j){
if(range[i][j].fi<=x && range[i][j].se>=x){
vec[i][j].pb(x);
move(pos_r,pos_c,i,j);
flag=1;break;
}
}
if(flag)break;
}
assert(flag);
}
for(int i=6;i>=pos_r;--i){
for(int j=6;j>=pos_c+(i==pos_r);--j){
solve(i,j,range[i][j].fi,range[i][j].se,vec[i][j]);
}
}
}
int main() {
for(int i=1;i<=6;++i){
for(int j=1;j<=6;++j){
if(i==1 && j==1){
f[i][j]=1;
continue;
}
if((i==1 && j==2) || (i==2 && j==1)){
f[i][j]=2;
continue;
}
f[i][j]=0;
int tmp=0;
for(int x=1;x<=i;++x)for(int y=1;y<=j;++y){
tmp+=f[x][y];
}
f[i][j]=tmp;
}
}
cerr<<f[6][6]<<endl;
cin>>n;
vector<int>a;
a.resize(n);
for(int i=0;i<n;++i){
cin>>a[i];
}
solve(1,1,1,n,a);
cout<<SZ(ans)<<endl;
for(int i=0;i<SZ(ans);++i){
cout<<ans[i].r<<" "<<ans[i].c<<" "<<ans[i].d<<" "<<ans[i].num<<endl;
}
return 0;
}
day9-猜拳
再解决最后一点细节就来更新。
day10-数学题
假设已经知道了\(p,q,k\)。我们可以用一个二元组,来描述\(n=p^kq^k\)的每个约数:二元组\((a,b)\)表示\(p^aq^b\) (\(0\leq a,b\leq k\))。
考虑将所有这样的二元组,按\(p^aq^b\)排序。然后做一个DP。设\(dp[i][x][y]\)表示考虑了前\(i\)个二元组,当前选出的二元组中,\(p\)的指数和为\(x\),\(q\)的指数和为\(y\),的方案数。由于同一个二元组可以选多次,我们可以按照完全背包的方法,把第一维去掉,然后每次从小到大枚举\(x,y\)。
答案就是\(dp[k][k]\)。
我们发现,这个DP的结果,与\(p,q\)具体是多少无关。因为我们做的DP是一个背包,与物品的顺序没有关系。只要选出一个物品的集合,那一定能有唯一的一种方法将其排好序变成一个序列。
既然与\(p,q\)无关,那么可以对每个\(k\),用上述的DP预处理它的答案。物品有\(k^2\)个,所以DP的时间复杂度是\(O(k^4)\)的。因为\(2^{24}\times3^{24}>10^{18}\),所以\(k\)最大不超过\(23\)。枚举每个\(k\)做一遍这样的DP,复杂度是\(23^5\)。
然后处理每个询问,最关键的问题是要求出\(k\)。可以从\(23\)到\(1\)枚举所有可能的\(k\),利用\(\texttt{C++}\)自带的\(\texttt{pow}\)函数对\(n\)开\(k\)次根(也就是\(\texttt{pow((long double)n,1.0/k)}\)),然后再检验这个开根结果(取整后)的\(k\)次方是否等于\(n\)。由于精度问题,取整最好把一定范围内上下几个数都试一遍。
设\(\texttt{pow}\)函数的时间复杂度是\(P(n)\),则总时间复杂度\(O(23^5 + T\times 23\times (P(n)+23))\)。其中最后\(P(n)+23\)也可以变成\(P(n)+\log 23\),取决于检验开根结果时,是否使用快速幂。
参考代码(片段。需添加读入优化):
// 请添加读入优化!!!
ll n;
ll ans[25],dp[25][25];
void init(){
for(int K=1;K<=23;++K){
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=0;i<=K;++i){
for(int j=(i==0);j<=K-(i==K);++j){
for(int x=0;x<=K-i;++x){
for(int y=0;y<=K-j;++y){
dp[x+i][y+j]+=dp[x][y];
}
}
}
}
ans[K]=dp[K][K];
cerr<<ans[K]<<endl;
}
}
inline ll qpow(ll x,int i){
ll y=1;
while(i){
if(i&1){
y*=x;
}
x*=x;
i>>=1;
}
return y;
}
void solve_case(){
cin>>n;
for(int i=23;i>=1;--i){
ll pq = pow((long double)n,1.0/i);
for(ll x=max(pq-1,1LL);x<=pq+1;++x){
ll pw=qpow(x,i);
// for(int j=1;j<=i;++j){
// pw*=x;
// }
if(pw==n){
cout<<ans[i]<<endl;
return;
}
}
}
cerr<<n<<endl;
assert(0);
}
int main() {
init();
int T;cin>>T;while(T--){
solve_case();
}
return 0;
}
day10-城市
对于两个城市 \(i,j\),如果 \(a_i\operatorname{and}a_j\neq 0\),题目说它们之间会有一条边权为 \(\text{lowbit}(a_i \operatorname{and} a_j)\) 的边。但其实,我们可以对所有 \(a_i\operatorname{and}a_j\) 里是 \(1\) 的二进制位,都连一条对应位权的边。显然这样不会改变答案。
于是问题转化为,对于一个点 \(i\),检查它的每个为 \(1\) 的二进制位 \(k\) (\(0\leq k<32\))。然后从 \(i\) 向所有二进制下第 \(k\) 位为 \(1\) 的点连一条边权为 \(2^k\) 的边。
既然是向某一类点集体连边,那么可以建虚点。也就是建 \(32\) 个点分别表示 \(k=0\dots 31\),然后从每个 \(i\) 向这些虚点连边,再从虚点向对应的 \(j\) 连边。这样总边数是 \(O(n\log a)\) 的。跑一个最短路,时间复杂度 \(O(n\log a\log n)\)。
代码略。
day10-好 ♂ 朋 ♂ 友
注意到,一个序列的前缀 \(\operatorname{or}\) 和只会变化 \(O(\log a)\) 次。因为每次的变化,相当于把一些位变成 \(1\),而某一位一旦从 \(0\) 变成 \(1\),就不会再改变了。
把询问离线。从大到小枚举询问的左端点。每次添加一个可能的,子段的左端点 \(i\)。考虑从 \(i\) 到 \(n\) 的这段序列,它的前缀 \(\operatorname{or}\) 和总共会变化 \(O(\log a)\) 次。这些变化的位置可以 \(O(n\log a)\) 预处理出来,做法是从后往前,递推求 \(\text{nxt}(i,j)\) 表示位置 \(i\) 后面第一个第 \(j\) 位为 \(1\) 的数在哪。
枚举这 \(O(\log a)\) 段东西,若数值合法,则考虑这一段对询问的贡献。用一个数组维护,以每个 \(r\) 为右端点的合法子段数量。则每次的贡献,相当于对这个数组做区间加。回答询问则相当于是区间求和。可以用线段树维护。
时间复杂度\(O(m \log n + n\log a\log n)\)。
参考代码:
// problem: nflsoj475
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
/* --------------- fast io --------------- */ // begin
namespace Fread {
const int SIZE = 1 << 21;
char buf[SIZE], *S, *T;
inline char getchar() {
if (S == T) {
T = (S = buf) + fread(buf, 1, SIZE, stdin);
if (S == T) return '\n';
}
return *S++;
}
} // namespace Fread
namespace Fwrite {
const int SIZE = 1 << 21;
char buf[SIZE], *S = buf, *T = buf + SIZE;
inline void flush() {
fwrite(buf, 1, S - buf, stdout);
S = buf;
}
inline void putchar(char c) {
*S++ = c;
if (S == T) flush();
}
struct NTR {
~ NTR() { flush(); }
} ztr;
} // namespace Fwrite
#ifdef ONLINE_JUDGE
#define getchar Fread :: getchar
#define putchar Fwrite :: putchar
#endif
namespace Fastio {
struct Reader {
template<typename T>
Reader& operator >> (T& x) {
char c = getchar();
T f = 1;
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
x = 0;
while (c >= '0' && c <= '9') {
x = x * 10 + (c - '0');
c = getchar();
}
x *= f;
return *this;
}
Reader& operator >> (char& c) {
c = getchar();
while (c == '\n' || c == ' ') c = getchar();
return *this;
}
Reader& operator >> (char* str) {
int len = 0;
char c = getchar();
while (c == '\n' || c == ' ') c = getchar();
while (c != '\n' && c != ' ') {
str[len++] = c;
c = getchar();
}
str[len] = '\0';
return *this;
}
Reader(){}
} cin;
const char endl = '\n';
struct Writer {
template<typename T>
Writer& operator << (T x) {
if (x == 0) { putchar('0'); return *this; }
if (x < 0) { putchar('-'); x = -x; }
static int sta[45];
int top = 0;
while (x) { sta[++top] = x % 10; x /= 10; }
while (top) { putchar(sta[top] + '0'); --top; }
return *this;
}
Writer& operator << (char c) {
putchar(c);
return *this;
}
Writer& operator << (char* str) {
int cur = 0;
while (str[cur]) putchar(str[cur++]);
return *this;
}
Writer& operator << (const char* str) {
int cur = 0;
while (str[cur]) putchar(str[cur++]);
return *this;
}
Writer(){}
} cout;
} // namespace Fastio
#define cin Fastio :: cin
#define cout Fastio :: cout
#define endl Fastio :: endl
/* --------------- fast io --------------- */ // end
const int MAXN = 1e5;
const int MAXM = 1e6;
const int LOGA = 29;
int n, m, K;
bool good[10];
int a[MAXN + 5], nxt[LOGA + 1];
vector<pii> qs[MAXN + 5];
ll ans[MAXM + 5];
class SegmentTree {
private:
ll sum[MAXN * 4 + 5], tag[MAXN * 4 + 5];
void push_down(int p, int l, int mid, int r) {
if (tag[p]) {
sum[p << 1] += tag[p] * (mid - l + 1);
tag[p << 1] += tag[p];
sum[p << 1 | 1] += tag[p] * (r - mid);
tag[p << 1 | 1] += tag[p];
tag[p] = 0;
}
}
void push_up(int p) {
sum[p] = sum[p << 1] + sum[p << 1 | 1];
}
void range_add(int p, int l, int r, int ql, int qr, ll v) {
if (ql <= l && qr >= r) {
sum[p] += v * (r - l + 1);
tag[p] += v;
return;
}
int mid = (l + r) >> 1;
push_down(p, l, mid, r);
if (ql <= mid) {
range_add(p << 1, l, mid, ql, qr, v);
}
if (qr > mid) {
range_add(p << 1 | 1, mid + 1, r, ql, qr, v);
}
push_up(p);
}
ll range_query(int p, int l, int r, int ql, int qr) {
if (ql <= l && qr >= r) {
return sum[p];
}
int mid = (l + r) >> 1;
push_down(p, l, mid, r);
ll res = 0;
if (ql <= mid) {
res = range_query(p << 1, l, mid, ql, qr);
}
if (qr > mid) {
res += range_query(p << 1 | 1, mid + 1, r, ql, qr);
}
return res;
}
public:
void range_add(int l, int r, ll v = 1) {
range_add(1, 1, n, l, r, v);
}
ll range_query(int l, int r) {
return range_query(1, 1, n, l, r);
}
SegmentTree() {}
} T;
int main() {
cin >> n >> m >> K;
for (int i = 1; i <= K; ++i) {
int x;
cin >> x;
good[x] = 1;
}
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= m; ++i) {
int l, r;
cin >> l >> r;
qs[l].push_back(mk(r, i));
}
for (int i = n; i >= 1; --i) {
vector<pii> events;
for (int j = 0; j <= LOGA; ++j) {
if ((a[i] >> j) & 1)
nxt[j] = i; // [i, n] 里最小的, 第 j 位为 1 的位置
if (nxt[j])
events.push_back(mk(nxt[j], 1 << j));
}
events.push_back(mk(i, 0));
sort(events.begin(), events.end());
int cur_num = 0;
for (int j = 0; j < SZ(events); ++j) {
int jj = j;
while (jj + 1 < SZ(events) && events[jj + 1].fi == events[j].fi)
++jj;
for (int t = j; t <= jj; ++t) {
cur_num |= events[t].se;
}
if (good[cur_num % 10]) {
int nxt_pos = ((jj == SZ(events) - 1) ? n + 1 : events[jj + 1].fi);
T.range_add(events[j].fi, nxt_pos - 1);
}
j = jj;
}
for (int j = 0; j < SZ(qs[i]); ++j) {
ans[qs[i][j].se] = T.range_query(i, qs[i][j].fi);
}
}
for (int i = 1; i <= m; ++i) {
cout << ans[i] << endl;
}
return 0;
}
day11-全国高中 IO 联赛一试
算期望比较麻烦。可以先求和,再除以总方案数 \(\frac{n!}{\prod_{j=1}^{k}c_j!}\)。
对于每种选项 \(i\),考虑以它为答案的每道题,对总和的贡献。钦定这道题做对了,其他题随便填的方案数是 \(\frac{(n-1)!}{\prod_{j=1}^{k}(c_j-[j=i])!}\)。这样的题有 \(a_i\) 道,所以答案就是:
因为是小数运算,我们不可能直接算这么大的阶乘。所以需要化简式子,得到:
直接算这个即可。时间复杂度 \(O(k)\)。
代码略。
day11-纸条
由于官方题解写的非常好,所以我直接丢官方题解了。
day11-全国高中 IO 联赛二试
考虑已知点集,求最小生成树的方法。
任取一个点为根(不一定是点集里的点)。设点 \(i\) 到根的距离为 \(d_i\)。把点集里所有点,按 \(d_i\) 从小到大排序。令第一个点(\(d_i\) 最小的点)作为生成树的根。从第二个点开始,每次选当前点前面的、与当前点距离最小的点,作为当前点(在生成树上)的父亲,并把当前点加入生成树。
这种做法的正确性证明(简要思路):
设按 \(d_i\) 从小到大排好序后,每个点的位置为 \(p_i\)。
只需要证明,存在一个生成树,满足以 \(p_{\text{root}}=1\) 的点 \(\text{root}\) 为根时,其他点都满足 \(p_{fa(i)}<p_i\)。其中 \(fa(i)\) 是点 \(i\) 在生成树上的父亲。
可以先任取一个最小生成树。然后取最大的、满足 \(p_{fa(u)}>p_{u}\) 的点 \(u\)。令 \(fa(u):=fa(fa(u))\)。可以证明,这样边权和一定不会变大。
并且由于 \(u\) 是最大的满足 \(p_{fa(u)}>p_u\) 的点,所以 \(p_{fa(fa(u))}\) 一定 \(<p_{fa(u)}\),因此有限次操作后,一定能消除所有 \(p_{fa(u)}>p_u\) 的点。
根据这种方法,我们求出本题答案就比较简单了。
利用期望的线性性,考虑个点到它(生成树上)的父亲的距离,对答案的贡献。枚举这个点 \(u\),把所有(按 \(d_i\) 排序后)排在 \(u\) 前面的点,按与 \(u\) 的距离排序。在这些点里,枚举 \(u\)(生成树上)的父亲 \(v\),则所有排在 \(v\) 前面的点,都一定不能在点集里。算出这样的概率,乘以 \(u,v\) 之间的距离,加入答案。
时间复杂度 \(O(n^2)\)。
day12-T2-枣子
考虑某个正整数 \(t\),它可以表示为 \(x^y\) (\(y>1\)) 的形式,当且仅当 \(y\) 是【 \(t\) 的所有质因子的次数】的约数。具体地,设分解质因数后 \(t=p_1^{e_1}p_2^{e_2}\cdots p_k^{e_k}\),则 \(y|\gcd(e_1,e_2,\dots,e_k)\)。设最大的、合法的 \(y\) 为 \(g(t)\),则 \(g(t)=\gcd(e_1,e_2,\dots,e_k)\),且所有其他的、合法的 \(y\) 都是 \(g(t)\) 的约数。
设 \(y = g(a)\),\(a=x^y\)。则原数 \(a^{(b^c)}\) 可以写成 \(x^{(yb^c)}\),且根据 \(g\) 函数的定义,\(x\) 不能再被表示为任何数的 \(>1\) 次幂。
我们先预处理一个 \(dp(i)\),表示一个形如 \(u^i\) 的数,有多少种表示方法。其中 \(u\) 将作为一个整体不再被拆解(不会被拆成任何数的 \(>1\) 次幂,你可以把 \(u\) 想象为上一段中的 \(x\) )。DP 的转移是考虑最底下的数,是 \(u\) 的几次方。例如,最底下的数是 \(u^j\) (\(j\) 是 \(i\) 的约数),那么它的指数就应该是 \(\frac{i}{j}\),并且这个指数还能继续被拆分,拆分它的方案数就是 \(dp(g(\frac{i}{j}))\),所以 \(dp(i) = \sum_{j|i}dp(g(\frac{i}{j}))\)。通过巧妙地枚举 \(\frac{i}{j}\)(例如将 \(i\) 分解质因数后 dfs),我们可以顺便知道\(g(\frac{i}{j})\)。设 DP 数组长度为 \(L\),这个 DP 复杂度是 \(O(L\sqrt{L})\) 的。
理论上,只要这个 DP 数组的大小高达 \(yb^c\) 这么大,我们就能直接报出答案。(不过答案并不是 \(dp(yb^c)\),因为题目要求枣子塔高度 \(\geq 3\),而我们 DP 时并没有考虑这个要求。不过这并不本质,只要稍微修改一下 DP 的定义就能解决这一问题)。
但显然,DP 数组不可能有 \(yb^c\) 这么大,所以这个算法还需改进。
考虑最终的结果(也就是这个等于 \(a^{(b^c)}\) 的枣子塔)里,最底下的数是什么。发现它一定可以被表示为 \(x^k\),其中 \(k\) 是 \(yb^c\) 的一个约数。如果枚举了 \(k\),此时的方案数就是:堆出值等于 \(\frac{yb^c}{k}\) 的、高度 \(\geq 2\) 的枣子塔的方案数,也就是 \(dp(g(\frac{yb^c}{k})) - 1\)。其中 \(-1\) 是去掉唯一的那个高度为 \(1\) 的枣子塔。
于是答案就等于:\(\sum_{k|yb^c}\left(dp(g(\frac{yb^c}{k})) - 1\right)\),令 \(i = \frac{yb^c}{k}\),则也可以更简洁地写成:
虽然 \(yb^c\) 极大,它的约数也极多,但将它分解质因数却并不困难,我们只要把 \(y\) 和 \(b\) 分别分解质因数,再合并起来即可。
知道了它的质因数构成后,我们枚举 \(g(i)\) 的值,设为 \(j\)(显然 \(j\) 小于等于 \(yb^c\) 质因子的最大次数,也就是 \(O(\log y+c\log b)\) ),此时 \(i\) 里所有质因子次数,都必须是 \(j\) 的倍数,那我们枚举每个质因子 \(p_t\),设它的出现次数为 \(e_t\),则方案数是 \(\prod_{t}(\lfloor\frac{e_t}{j}\rfloor +1 )\)。但这还包含了 \(g(i)\) 是 \(j\) 的倍数的情况,将它们减掉即可:暴力枚举倍数,总复杂度是调和级数,这是个经典套路了。
因为 \(g(i)\) 的值不超过 \(O(\log y + c\log b)=O(\log\log a + c \log b)\),所以 DP 数组也只需要预处理这么长。设 \(L = c\log b\),则该算法的总时间复杂度是 \(O(L\sqrt{L} +TL\log L)\)。可以通过本题。
参考代码:
//problem:nflsoj480
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 3e4, LOG = 20;
const int MAXG = (MAXN + 1) * LOG;
const int MOD = 998244353;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
int gcd(int x, int y) { return (!y) ? x : gcd(y, x % y); }
int a, b, c;
vector<pii> prm_a, prm_b;
vector<pii> decompose(int x) {
vector<pii> p;
for(int i = 2; i * i <= x; ++i) {
if(x % i == 0) {
p.pb(mk(i, 0));
while(x % i == 0) {
x /= i;
p.back().se++;
}
}
}
if(x != 1) {
p.pb(mk(x, 1));
}
return p;
}
int dp[MAXG + 5];
void dfs(int idx, int g, int v, int id, const vector<pii>& p) {
if(idx == SZ(p)) {
if(v > 1) {
add(dp[id], dp[g]);
}
return;
}
int vv = v;
for(int i = 0; i <= p[idx].se; ++i) {
dfs(idx + 1, gcd(g, i), vv, id, p);
vv *= p[idx].fi;
}
}
void init() {
dp[1] = 1;
for(int i = 2; i <= MAXG; ++i) {
dp[i] = 1;
vector<pii> p = decompose(i);
dfs(0, 0, 1, i, p);
}
// cerr << "yes" << endl;
// for(int i = 1; i <= 10; ++i) cerr << dp[i] << " "; cerr << endl;
}
int w[MAXG + 5];
void solve_case() {
cin >> a >> b >> c;
prm_a = decompose(a);
prm_b = decompose(b);
int g = prm_a[0].se;
for(int i = 1; i < SZ(prm_a); ++i) g = gcd(g, prm_a[i].se);
vector<pii> tmp = decompose(g);
vector<pii> p;
int i = 0, j = 0;
while(i < SZ(tmp) && j < SZ(prm_b)) {
if(tmp[i].fi == prm_b[j].fi) {
p.pb(mk(tmp[i].fi, tmp[i].se + prm_b[j].se * c));
++i; ++j;
} else if(tmp[i].fi < prm_b[j].fi) {
p.pb(mk(tmp[i].fi, tmp[i].se));
++i;
} else {
p.pb(mk(prm_b[j].fi, prm_b[j].se * c));
++j;
}
}
while(i < SZ(tmp)) {
p.pb(mk(tmp[i].fi, tmp[i].se));
++i;
}
while(j < SZ(prm_b)) {
p.pb(mk(prm_b[j].fi, prm_b[j].se * c));
++j;
}
int lim = 0;
for(int j = 0; j < SZ(p); ++j) {
assert(p[j].se <= MAXG);
ckmax(lim, p[j].se);
}
for(int i = 1; i <= lim; ++i) {
w[i] = 1;
for(int j = 0; j < SZ(p); ++j) {
w[i] = (ll)w[i] * (p[j].se / i + 1) % MOD;
}
sub(w[i], 1);
}
int ans = 0;
for(int i = lim; i >= 2; --i) {
for(int j = i + i; j <= lim; j += i) {
sub(w[i], w[j]);
}
// if(w[i]) cerr << i << " " << w[i] << endl;
ans = ((ll)ans + (ll)w[i] * mod2(dp[i] - 1)) % MOD;
}
cout << ans << endl;
}
int main() {
// freopen("jujube.in", "r", stdin);
// freopen("jujube.out", "w", stdout);
init();
int T; cin >> T; while(T--) {
solve_case();
}
return 0;
}
day12-T3-开关
题目要求的是集合数量,也就是忽略顺序的。不过由于题目强制了 \(m\) 次操作互不相同,我们可以先求出带顺序的答案,再除以 \(m!\) 。具体来说,记原答案是 \(f_m(v)\),我们令 \(g_m(v) = m!f_m(v)\),现在考虑 \(g_m(v)\) 怎么求。
考虑枚举前 \(m-1\) 个是啥,那么最后一个也就确定了。这样方案数是 \({2^n\choose m-1}(m-1)!\)。
但这样可能会计算到一些不合法的情况,也就是我们的最后一次操作,和前面某个操作相等。那么由于这两个操作相等,它们异或和为 \(0\),所以其他的 \(m-2\) 个操作的异或和就是我们想要的值(前 \(v\) 位为 \(1\) 的这个二进制数),并且这 \(m-2\) 个操作一定互不相等(因为是通过组合数 \({2^n\choose m-1}\) 选出来的)。也就是说,这 \(m-2\) 个操作,形成了一个子问题,它们的方案数就是:\(g_{m-2}(v)\)。
如果枚举,最后一次操作和前面哪次操作相等,那么不合法的总方案数就是:\((m-1)(2^n-(m-2))g_{m-2}(v)\)。其中 \(m-1\) 是枚举哪次操作相等,\((2^n-(m-2))\) 是这两个操作可以选择的值(不能和其他 \(m-2\) 个操作相同),最后其他操作的方案数就是 \(g_{m-2}(v)\)。
预处理出 \({2^n\choose 0},{2^n\choose 1},{2^n\choose 2},\dots ,{2^n\choose m}\) 的值,然后就能 \(O(m)\) 递推了。
总时间复杂度 \(O(Tm)\)。
参考代码:
//problem:nflsoj481
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXM = 1000;
const int MOD = 19260817;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
inline int pow_mod(int x, int i) {
int y = 1;
while(i) {
if(i & 1) y = (ll)y * x % MOD;
x = (ll)x * x % MOD;
i >>= 1;
}
return y;
}
int fac[MAXM + 5], ifac[MAXM + 5], inv[MAXM + 5];
void facinit() {
fac[0] = 1;
for(int i = 1; i <= MAXM; ++i) fac[i] = (ll)fac[i - 1] * i % MOD;
ifac[MAXM] = pow_mod(fac[MAXM], MOD - 2);
for(int i = MAXM - 1; i >= 0; --i) ifac[i] = (ll)ifac[i + 1] * (i + 1) % MOD;
for(int i = 1; i <= MAXM; ++i) inv[i] = (ll)ifac[i] * fac[i - 1] % MOD;
}
int n, m, v;
int _2n, comb_2n[MAXM + 5]; // C(2^n, 1...m)
int ans[MAXM + 5];
void case_init() {
// 已知 m,n
comb_2n[0] = 1;
_2n = pow_mod(2, n);
for(int i = 1; i <= m; ++i) {
comb_2n[i] = (ll)comb_2n[i - 1] * mod2(_2n - i + 1) % MOD * inv[i] % MOD;
}
}
void solve_case() {
cin >> n >> m >> v;
case_init();
ans[0] = (!v);
ans[1] = 1;
for(int i = 2; i <= m; ++i) {
ans[i] = mod2((ll)comb_2n[i - 1] * fac[i - 1] % MOD - (ll)ans[i - 2] * mod2(_2n - (i - 2)) % MOD * (i - 1) % MOD);
}
int Ans = (ll)ans[m] * ifac[m] % MOD;
cout << Ans << endl;
}
int main() {
facinit();
int T; cin >> T; while(T--) {
solve_case();
}
return 0;
}
day12-A
考虑 \(S\) 和 \(T\) 从前往后第一个不同的位置 \(x\),以及从后往前第一个不同的位置 \(y\)。 一个核心的观察是,如果存在合法的翻转方案,最短的翻转串一定就是 \([x,y]\)。并且其他翻转串一定都是 \([x-i, y+i]\) 的形式,且 \(i\) 是 \(0,1,\dots\) 连续的一段。如果知道了 \(S,T\) 的哈希情况,这个最大的 \(i\) 我们可以二分出来。
可以用线段树维护哈希值,在线段树上二分。
注意特判 \(S,T\) 相等的情况,此时所有合法的翻转串,就是 \(T\) 里的回文串。由于 \(T\) 不会变,我们可以二分 + 哈希预处理出来。
时间复杂度 \(O((|S| + m)\log |S|)\)。
day15-T1-小 D 与原题
相当于要把 \((1,2),(1,3),\dots,(1,n),(2,3),(2,4),\dots,(2,n),\dots\dots,(n-1,n)\) 这 \(\frac{n(n-1)}{2}\) 对元素,分成 \(n-1\) 组,使得每组不出现重复的数字。
首先,\((1,2),(1,3),\dots,(1,n)\),这 \(n-1\) 对元素中,任意两对都不可能出现在同一组(因为它们都有 \(1\)),所以不妨先将它们分别放在每一组的第一个。
现在,每一组还剩 \(n-2\) 个数字(即 \(\frac{n-2}{2}\) 对元素)要放。对于第 \(i\) 组(也就是第一对元素为 \((1,i)\) 的组),我们维护两个指针 \(x,y\),初始时都等于 \(i\)。每次构造下一对数字前,令 \(x\) 往前移动一步,\(y\) 往后移动一步。具体来说:
这样它们最终会各自走 \(\frac{n-2}{2}\) 步,恰好经过所有 \(n-2\) 个数字。相当于两个人,从同一起点出发,分别顺时针、逆时针走一个半圆,最后拼成一个整圆。所以肯定同一组里,一定不会经过重复的数字。
另外,可以发现,同一组里的每一对 \((x,y)\),在半圆上的对称点相等(就是出发时的 \(i\)),不同组则不等。也可以用公式表示为,每一组具有一个唯一的 \(((x-2)+(y-2))\bmod(n-1)\) 的值。所以不同组里,一定不会出现同一对 \((x,y)\)。
综合上述两点,我们的构造方案是正确的。
参考代码(片段):
int main() {
int n; cin >> n;
int len = n / 2 - 1;
for(int i = 2; i <= n; ++i) {
int x = i, y = i;
cout << 1 << " " << x << " ";
for(int j = 1; j <= len; ++j) {
x = (x == 2 ? n : x - 1);
y = (y == n ? 2 : y + 1);
cout << x << " " << y << " ";
}
cout << endl;
}
return 0;
}
day15-T2-小 D 与随机
day15-T3-小 D 与游戏
特判 \(n\leq 3\) 的情况。以下只讨论 \(n\geq4\)。
考虑把原串里 \(a,b,c\) 分别换成数字 \(0,1,2\)。发现每次操作字符串所有位上数字的和在 \(\bmod 3\) 意义下不变。那是不是所有数字和 \(\bmod3\) 相等的串,都能通过若干次操作相互达到呢?带着这个疑问,我们爆搜出 \(n=4\) 的情况。发现可以分成三类:
- 如果整个串所有字符都相等(即 \(S_1=S_2=S_3=S_4\)),那么它不能做任何变化,所以答案是 \(1\)。
- 除第 1 类外,如果存在一对相邻的字符相等,则答案是 \(19\)。
- 如果不存在一对相邻的字符相等,则答案是 \(20\)。
这比较好解释,因为如果不存在相邻、相等的字符,那这个串一定不是通过操作得到的(因为只要经过了操作,就一定存在相邻、相等的字符),所以这样的串答案比其他串多了一个它自己的初始状态。
又注意到在 \(n=4\) 时,对于 \(\bmod3\) 的每个余数,串的总数只有 \(1+18+8=27\) 种(分别是上述三类串的数量)。所以对于 \(n=4\),我们得到的结论是:
对于第 2 类的串,它能到达任意一个和它同余的,第 1 或第 2 类的串。
对于第 3 类的串,它能到达任意一个和它同余的,第 1 或第 2 类的串;不过由于还要算上它自己的初始状态,所以它的答案比第 2 类的串多 \(1\)。
当 \(n>4\) 时,可以通过打表验证或归纳证明,这个结论是正确的。
于是问题转化为求第 2 类串的数量,也就是:字符之和 \(\bmod3\) 与原串同余的、存在一对相邻字符相等的,串的数量。可以做一个简单 DP。设 \(dp[i][x\in\{0,1,2\}][y\in\{0,1,2\}][z\in\{0,1\}]\),表示考虑了前 \(i\) 位,最后一位是 \(x\),字符之和 \(\bmod3\) 是 \(y\),是否已经存在一对相邻、相等的字符,这样的串的数量。
时间复杂度 \(O(n)\)。
参考代码(片段):
const int MAXN = 2e5;
const int MOD = 998244353;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
int n, dp[MAXN + 5][3][3][2];
char s[MAXN + 5];
map<vector<int>, bool> mp;
void dfs(const vector<int>& v) {
if(mp.count(v)) return;
mp[v] = 1;
for(int i = 0; i <= SZ(v) - 2; ++i) if(v[i] != v[i + 1]) {
int x = 0;
for(; ; ++x) {
if(x != v[i] && x != v[i + 1]) break;
}
vector<int> vv = v;
vv[i] = vv[i + 1] = x;
dfs(vv);
}
}
int main() {
cin >> (s + 1);
n = strlen(s + 1);
if(n <= 3) {
vector<int> st;
for(int i = 1; i <= n; ++i) {
st.pb(s[i] - 'a');
}
dfs(st);
cout << mp.size() << endl;
return 0;
}
bool all_same = true;
for(int i = 2; i <= n; ++i) all_same &= (s[i] == s[1]);
if(all_same) { cout << 1 << endl; return 0; }
dp[1][0][0][0] = dp[1][1][1][0] = dp[1][2][2][0] = 1;
for(int i = 2; i <= n; ++i) {
for(int j = 0; j < 3; ++j) {
for(int k = 0; k < 3; ++k) {
for(int l = 0; l <= 1; ++l) if(dp[i - 1][j][k][l]) {
for(int c = 0; c < 3; ++c) {
add(dp[i][c][(k + c) % 3][l | (c == j)], dp[i - 1][j][k][l]);
}
}
}
}
}
int sum = 0;
for(int i = 1; i <= n; ++i) sum += (s[i] - 'a');
sum %= 3;
int ans = 1;
for(int i = 2; i <= n; ++i) if(s[i] == s[i - 1]) { ans = 0; break; }
for(int j = 0; j < 3; ++j) add(ans, dp[n][j][sum][1]);
cout << ans << endl;
return 0;
}