T 营模拟,所以四道题,梦回 NOIP 前模拟赛。
A 石子游戏(stone)
点击查看
天天放博弈论,博弈论是你爹吗?先打表,发现 B 赢麻了,A 赢的时候几乎全是 \(1\),前面全是 \(1\),后面带了一个 \(2\) 的时候 A 也会赢,由于 A 每次拿一堆,B 每次拿两堆,直接猜答案与 \(1\),\(2\),和其他数的个数有关。
考虑用 DP 打表,\(f_{i,j,k,0/1}\) 表示有 \(i\) 个 \(1\),\(j\) 个 \(2\),\(k\) 个其他,发现可以自己转移自己,但是我们已经钦定了 答案与 1,2,和其他数的个数有关
,所以其他数不能变成其他数,只能变成 \(1,2\),所以直接不管,写完后发现这个是对的,并且能过题。
那如果 \(n\) 很大呢,再次观察题意和打的表,首先有一个很简单的情况,如果全是 \(1\),那么当 \(n\equiv 0\pmod 3\) 时,Alice 必败,考虑在全是 \(1\) 的基础上再加一个不为 \(1\) 的数 \(x\),Alice 可以选择拿走 \(x\) 或者把 \(x\) 变成 \(1\),两种总有一种会赢,得出另一个结论,只有一个不为 \(1\) 的数时,Alice 先手必胜。
考虑现在有 \(k\) 个数大于 \(1\)。
考虑 Bob 的策略,他一定要尽量避免 \(k=1\),
如果 \(k=2\),那么 Bob 可以增加 \(0,1,2\) 个 \(1\),必胜。如果 \(k=1\),如果这个数大于 \(2\),那么 Bob 可以增加 \(0,1,-1\) 个 \(1\),必胜,如果这个数是 \(2\),那 Bob 只能增加 \(0,-1\) 个 \(1\),如果同时 \(n-1\equiv 2\pmod 3\),那么 Bob 必败,所以这种情况要求一堆 \(1\),一个 \(2\),一个 \(x\),\(n-2\not\equiv 0\pmod 3\) Alice 才能必胜,他可以增加 \(0,1\) 个 \(1\)。
其他情况 Bob 都可以及时调整。
点击查看代码
#include<bits/stdc++.h>
#define fi first
#define se second
#define pii std::pair<int,int>
#define eb emplace_back
#define pb push_back
typedef long long ll;
typedef unsigned long long ull;
std::mt19937_64 myrand(std::chrono::high_resolution_clock::now().time_since_epoch().count());
inline int R(int n){return myrand()%n+1;}
inline int read(){char ch=getchar();int x=0,f=1;for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);return x*f;}
inline void Min(int &x,int y){if(x>y)x=y;}
inline void Max(int &x,int y){if(x<y)x=y;}
int n,b[105];
signed main(){// freopen("in.in","r",stdin);freopen("out.out","w",stdout);std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);int T=read();while(T--){int a=0;n=read();for(int i=1;i<=n;++i)b[i]=read(),a+=b[i]==1;if(a==n){if(n%3)std::cout<<"Win\n";else std::cout<<"Lose\n";continue;}if(a==n-1){std::cout<<"Win\n";continue;}if(a==n-2){bool pd=0;for(int i=1;i<=n;++i)if(b[i]==2)pd=1;if(pd&&n%3!=2)std::cout<<"Win\n";else std::cout<<"Lose\n";continue;}std::cout<<"Lose\n";}
}
B 树上字符串(treestr)
不难想到预处理 \(i,j\) 区间在树上的信息,这个复杂度肯定是 \(\mathcal{O}(n|S|^2)\) 了,可以想到倍增或者树剖来拼接信息,但是这样会多一个 \(\log\),考虑还是用树上前缀和来做这个,\(f_{i,j,k}\) 表示从根到 \(i\),\(S[j,k]\) 出现的次数,发现拼接需要容斥,类似于一个退背包的东西,直接从小到大退就行了。
点击查看代码
#include<bits/stdc++.h>
#define fi first
#define se second
#define pii std::pair<int,int>
#define eb emplace_back
#define pb push_back
typedef long long ll;
typedef unsigned long long ull;
std::mt19937_64 myrand(std::chrono::high_resolution_clock::now().time_since_epoch().count());
inline int R(int n){return myrand()%n+1;}
inline int read(){char ch=getchar();int x=0,f=1;for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);return x*f;}
inline void Min(int &x,int y){if(x>y)x=y;}
inline void Max(int &x,int y){if(x<y)x=y;}
const int N=1e5+10,mod=998244353;
inline void W(int &x,int y){x=(x+y)%mod;}
int n,q,d[N][32][32],st[20][N],len,fu[N],tmp1[N],tmp2[N],dfn[N],dn;
char S[N],T[N];
std::vector<int> e[N],v[N];
inline int get(int x,int y){return dfn[x]<dfn[y]?x:y;}
inline void dfs(int x,int fa){fu[x]=fa;for(int i=1;i<=len;++i)for(int j=1;j<=len;++j)d[x][i][j]=d[fa][i][j];for(int y:v[x]){W(d[x][y][y],1);for(int i=y+1;d[fa][i][y+1];++i)W(d[x][i][y],d[fa][i][y+1]);for(int i=y-1;d[fa][i][y-1];--i)W(d[x][i][y],d[fa][i][y-1]);}st[0][dfn[x]=++dn]=fa;for(int v:e[x])if(v^fa)dfs(v,x);if(x==1)for(int i=1;i<=std::__lg(n);++i)for(int j=1;j+(1<<i)-1<=n;++j)st[i][j]=get(st[i-1][j],st[i-1][j+(1<<i-1)]);
}
inline int LCA(int u,int v){if(u==v)return u;if((u=dfn[u])>(v=dfn[v]))std::swap(u,v);int d=std::__lg(v-u++);return get(st[d][u],st[d][v-(1<<d)+1]);
}
inline int query(int u,int v){int lca=LCA(u,v),f=fu[lca],res=0;for(int i=1;i<=len;++i)tmp1[i]=d[u][i][1],tmp2[i]=d[v][i][len];tmp1[0]=1;tmp2[len+1]=1;for(int i=1;i<=len;++i)for(int j=i;j;--j)W(tmp1[i],-1ll*tmp1[j-1]*d[f][i][j]%mod);f=lca;for(int i=len;i;--i)for(int j=i;j<=len;++j)W(tmp2[i],-1ll*tmp2[j+1]*d[f][i][j]%mod);for(int i=0;i<=len;++i)W(res,1ll*tmp1[i]*tmp2[i+1]%mod);return (res+mod)%mod;
}
signed main(){// freopen("in.in","r",stdin);freopen("out.out","w",stdout);std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);std::cin>>n>>q;for(int i=1;i<n;++i){int u,v;std::cin>>u>>v;e[u].eb(v);e[v].eb(u);}std::cin>>T+1>>S+1;len=strlen(S+1);for(int i=1;i<=n;++i)for(int j=1;j<=len;++j)if(T[i]==S[j])v[i].eb(j);dfs(1,0);for(int i=1;i<=q;++i){int u,v;std::cin>>u>>v;std::cout<<query(u,v)<<"\n";}
}
C 区间划分(divid)
注意到这个好区间的数量并不多,最多只有 \(n\log n\) 个,如果能找到所有好区间,就能直接用这些区间来 DP 了,如果一个区间的长度为 \(len\),其中最大的一个数为 \(2^a\),那么 \(x\in[a,a+\log len]\),所以可以直接暴力分治来找到所有区间,每次找到区间中最大值的位置 \(mid\),然后以小的区间中的位置为端点来找另一个区间的端点,用哈希存前缀和即可,需要一个巨大无比的模数。
点击查看代码
#include<bits/stdc++.h>
#define fi first
#define se second
#define pii std::pair<int,int>
#define eb emplace_back
#define pb push_back
typedef long long ll;
typedef unsigned long long ull;
std::mt19937_64 myrand(std::chrono::high_resolution_clock::now().time_since_epoch().count());
inline int read(){char ch=getchar();int x=0,f=1;for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);return x*f;}
inline void Min(int &x,int y){if(x>y)x=y;}
inline void Max(int &x,int y){if(x<y)x=y;}
const int N=6e5+10,mod=1e9+7,M=1e6+30;
const ll P=100000000000000003;
inline void W(int &x,int y){x=(x+y)%mod;}
int n,a[N],st[20][N],f[N],lg[N];
ll p[M],s[N];
std::unordered_map<ll,int> mp;
inline int get(int x,int y){return a[x]<a[y]?y:x;}
inline int Get(int l,int r){int d=lg[r-l+1];return get(st[d][l],st[d][r-(1<<d)+1]);}
inline void sol(int l,int r){if(l>r)return ;if(l==r)return W(f[l],f[l-1]),void();int mid=Get(l,r);sol(l,mid-1);int L=a[mid],R=L+lg[r-l+1];if(mid-l+1<=r-mid){for(int i=l;i<=mid;++i){for(int ai=L;ai<=R;++ai){ll ne=(s[i-1]+p[ai])%P;if(mp.find(ne)!=mp.end()){int wc=mp[ne];if(wc>=mid&&wc<=r)W(f[wc],f[i-1]);}}}}else{for(int i=mid;i<=r;++i){for(int ai=L;ai<=R;++ai){ll ne=(s[i]-p[ai]+P)%P;if(mp.find(ne)!=mp.end()){int wc=mp[ne];if(wc>=l-1&&wc<mid)W(f[i],f[wc]);}}}}sol(mid+1,r);
}
signed main(){// freopen("in.in","r",stdin);freopen("out.out","w",stdout);std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);n=read();f[0]=1;lg[0]=-1;for(int i=1;i<=n;++i)lg[i]=lg[i>>1]+1;p[0]=1;for(int i=1;i<=M-10;++i)p[i]=p[i-1]*2%P;mp[0]=0;for(int i=1;i<=n;++i)a[i]=read(),s[i]=(s[i-1]+p[a[i]])%P,st[0][i]=i,mp[s[i]]=i;for(int i=1;i<=std::__lg(n);++i)for(int j=1;j+(1<<i)-1<=n;++j)st[i][j]=get(st[i-1][j],st[i-1][j+(1<<i-1)]);sol(1,n);std::cout<<f[n]<<'\n';
}
D 机器(machine)
经典水龙头接水问题,一个人的贡献是多少个人(包括自己)在等他,那么最小代价的方案就是前 \(K\) 大有一个人等,第二 \(K\) 大有两个人等,以此类推。
题目没要求在线,可以先把所有人都拿出来,每次操作就相当于停用一个人,启用一个人。把所有人从大到小排序后放到一个 \(m\) 行乘 \(K\) 列的矩形里,第 \(i\) 行的系数是 \(i\),假设两个人的位置分别是 \(l,r(l<r)\),那么他们之间的人都会往前平移一个,只有每行第一个人的系数才会变,\((l>r)\) 同理。
如果 \(m\) 比较小,可以每行维护,如果 \(K\) 比较小,可以每列维护,对 \(K\) 根号分治后做到 \(\mathcal{O}(n\sqrt n\log n)\),过不了。
考虑分块,\(d_{i,j}\) 表示第 \(i\) 个块中模 \(K\) 为 \(j\) 的值的和,注意到每个块中只有 \(B\) 个有效位置,所以可以做一个偏移的东西,再记一个 \(s_i\) 表示第 \(i\) 个块中有效值的数量即可。
点击查看代码
#include<bits/stdc++.h>
#define fi first
#define se second
#define pii std::pair<int,int>
#define eb emplace_back
#define pb push_back
typedef long long ll;
typedef unsigned long long ull;
std::mt19937_64 myrand(std::chrono::high_resolution_clock::now().time_since_epoch().count());
inline int R(int n){return myrand()%n+1;}
inline int read(){char ch=getchar();int x=0,f=1;for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);return x*f;}
inline void write(__int128 x){if(x>=10)write(x/10);putchar(x%10+'0');}
inline void Min(int &x,int y){if(x>y)x=y;}
inline void Max(int &x,int y){if(x<y)x=y;}
const int N=6e5+10,LEN=800;
int n,K,A[N],Q,tot,num[LEN],match[N],len,ID[N];
ll s[LEN][LEN];
__int128 ans;
bool vis[N];
struct NODE{int x,id;}a[N],b[N];
struct QU{int l,r;}q[N];
inline ll c(int rk,int pos){return 1ll*((rk-1)/K+1)*a[pos].x;}
inline void pro(int id){int l=(id-1)*len+1,r=std::min(tot,l+len-1);for(int i=0;i<=len;++i)s[id][i]=0;num[id]=0;for(int i=l,now=0;i<=r;++i)if(vis[i])s[id][now++%K]+=a[i].x,num[id]++;
}
inline ll get(int id,int x,int pre){int zc=((x-pre)%K+K)%K;if(zc>=len)return 0;return s[id][zc];}
inline void work(int l,int r){int lid=ID[l],rid=ID[r];if(l<r){int rk=0;for(int i=1;i<lid;++i)rk+=num[i];for(int i=l;ID[i]==lid;--i)rk+=vis[i];ans-=c(rk,l);vis[l]=0,vis[r]=1;if(lid==rid){for(int i=l+1;i<r;++i){rk+=vis[i];if(vis[i])ans-=c(rk,i)-c(rk-1,i);}ans+=c(rk,r);pro(lid);}else{for(int i=l+1;ID[i]==lid;++i){rk+=vis[i];if(vis[i])ans-=c(rk,i)-c(rk-1,i);}for(int i=lid+1;i<rid;++i)ans-=get(i,0,rk),rk+=num[i];for(int i=(rid-1)*len+1;i<r;++i){rk+=vis[i];if(vis[i])ans-=c(rk,i)-c(rk-1,i);}ans+=c(rk,r);pro(lid);pro(rid);}}else{std::swap(l,r);std::swap(lid,rid);int rk=0;for(int i=1;i<lid;++i)rk+=num[i];vis[l]=1;vis[r]=0;for(int i=l;ID[i]==lid;--i)rk+=vis[i];ans+=c(rk,l);if(lid==rid){for(int i=l+1;i<r;++i){rk+=vis[i];if(vis[i])ans+=c(rk,i)-c(rk-1,i);}ans-=c(rk,r);pro(lid);}else{for(int i=l+1;ID[i]==lid;++i){rk+=vis[i];if(vis[i])ans+=c(rk,i)-c(rk-1,i);}for(int i=lid+1;i<rid;++i)ans+=get(i,K-1,rk-1),rk+=num[i];for(int i=(rid-1)*len+1;i<r;++i){rk+=vis[i];if(vis[i])ans+=c(rk,i)-c(rk-1,i);}ans-=c(rk,r);pro(lid),pro(rid);}}write(ans);putchar('\n');
}
signed main(){// freopen("in.in","r",stdin);freopen("out.out","w",stdout);std::ios::sync_with_stdio(false);std::cin.tie(0);std::cout.tie(0);n=read(),K=read();for(int i=1;i<=n;++i)A[i]=read(),a[++tot]={A[i],i},b[i]={A[i],i};int Q=read();for(int i=1;i<=Q;++i){int p=read(),x=read();q[i]={b[p].id,n+i};b[p]={x,n+i};a[++tot]={x,n+i};}std::sort(a+1,a+tot+1,[](NODE A,NODE B){return A.x>B.x;});len=std::sqrt(tot);std::sort(A+1,A+n+1,[](int a,int b){return a>b;});for(int i=1;i<=n;++i)ans+=1ll*((i-1)/K+1)*A[i];for(int i=1;i<=tot;++i)match[a[i].id]=i,ID[i]=(i-1)/len+1;for(int i=1;i<=n;++i)vis[match[i]]=1;for(int i=1;i<=Q;++i)q[i].l=match[q[i].l],q[i].r=match[q[i].r];for(int i=1;i<=tot;i+=len)pro(ID[i]);for(int i=1;i<=Q;++i)work(q[i].l,q[i].r);
}
总结
T2 不会大傻逼,赛时感觉容斥不了就把正解弃了。T1 博弈论不会大傻逼。