多校A层冲刺NOIP2024模拟赛03
\(T1\) A. 五彩斑斓(colorful) \(90/100pts\)
-
部分分
- \(20pts\) :枚举左上 \((k,h)\) 、右下端点 \((i,j)\) ,时间复杂度为 \(O(n^{2}m^{2})\) 。
- \(90/100pts\) :
-
当 \(a_{i,j} \ne a_{k,j}\) 时任意的 \(h \in [1,j]\) 都符合题意、
-
不妨钦定 \(a_{i,j}=a_{k,j}\) ,则符合题意的 \(h \in [1,j]\) 必须满足 \(a_{k,h} \ne a_{i,j} \lor a_{i,h}=a_{i,j}\) 。
-
bitset
记录下 \((i,j)\) 前与 \(a_{i,j}\) 的相等情况。再将两者或一下求 \(1\) 的个数即可。 -
时间复杂度为 \(O(\frac{n^{2}m \log m}{w})\) 。
点击查看代码
int a[410][410]; bitset<410>vis[410][410],tmp; int main() {freopen("colorful.in","r",stdin);freopen("colorful.out","w",stdout);int n,m,i,j,k;ll ans=0;scanf("%d%d",&n,&m);for(i=1;i<=n;i++){for(j=1;j<=m;j++){scanf("%d",&a[i][j]);for(k=1;k<=j;k++){vis[i][j][k]=(a[i][k]!=a[i][j]);}for(k=1;k<=i;k++){if(a[i][j]!=a[k][j]){ans+=j;}else{tmp=vis[k][j]|vis[i][j];ans+=tmp.count();}}}}printf("%lld\n",ans);fclose(stdin);fclose(stdout);return 0; }
-
-
正解
- 正难则反,考虑统计四个角都相等的子矩阵数列,再用总子矩阵 \(\frac{nm(n+1)(m+1)}{4}\) 减去。
- 仍考虑枚举 \(a_{i,j}\) 和 \(a_{k,j}\) 作为右上和右下端点,钦定 \(a_{i,j}=a_{k,j}\) ,开个桶计算 \(h \in [1,j]\) 中满足 \(a_{k,h}=a_{i,h}=a_{k,j}=a_{i,j}\) 的个数即可。
- 时间复杂度为 \(O(n^{2}m)\) 。
点击查看代码
ll a[410][410],cnt[1000010]; int main() {freopen("colorful.in","r",stdin);freopen("colorful.out","w",stdout);ll n,m,ans,i,j,k;cin>>n>>m;ans=n*(n+1)*m*(m+1)/4;for(i=1;i<=n;i++){for(j=1;j<=m;j++){cin>>a[i][j];}}for(i=1;i<=n;i++){for(k=1;k<=i;k++) {for(j=1;j<=m;j++) {if(a[i][j]==a[k][j]){cnt[a[i][j]]++;ans-=cnt[a[i][j]];}}for(j=1;j<=m;j++) {if(a[i][j]==a[k][j]){cnt[a[i][j]]--;}}}}cout<<ans<<endl;fclose(stdin);fclose(stdout);return 0; }
\(T2\) B. 错峰旅行(travel) \(70pts\)
-
对时间偏移是显然的。
-
记 \(V=t-s\) 。
-
部分分
- \(45 \%\) :
-
设 \(f_{i,j}\) 表示第 \(i\) 天在城市 \(j\) 的方案数,状态转移方程为 \(f_{i,j}=check(i,j) \times \sum\limits_{k=1}^{f_{i-1,k}}\) ,其中 \(check(i,j)=\begin{cases} 1 & 城市 j 在第 i 天不拥挤 \\ 0 & 城市 j 在第 i 天拥挤 \end{cases}\) ,边界为 \(f_{0,i}=check(0,i)\) 。
-
判断某个城市在转移过程中是否拥挤用单调数据结构维护一下即可。
-
观察到 \(f\) 的转移过程可以前缀和优化,时间复杂度为 \(O(nV)\) ,可以获得 \(50pts\) 。
点击查看代码
const ll p=1000000007; ll f[2][5010]; vector<pair<ll,ll> >e[5010]; bool cmp(pair<ll,ll> a,pair<ll,ll> b) {return a.first>b.first; } int main() {freopen("travel.in","r",stdin);freopen("travel.out","w",stdout);ll n,m,s,t,x,l,r,ans=0,sum,i,j,k;cin>>n>>m>>s>>t;t-=s;for(i=1;i<=m;i++){cin>>x>>l>>r;l-=s;r-=s;e[x].push_back(make_pair(l,r));}for(i=1;i<=n;i++){sort(e[i].begin(),e[i].end(),cmp);f[0][i]=(!(e[i].empty()==0&&e[i].back().first<=0&&0<=e[i].back().second));}for(i=1;i<=t;i++){sum=0;for(j=1;j<=n;j++){sum=(sum+f[(i-1)&1][j])%p;}for(j=1;j<=n;j++){f[i&1][j]=0;while(e[j].empty()==0&&e[j].back().second<i){e[j].pop_back();}f[i&1][j]=sum*(!(e[j].empty()==0&&e[j].back().first<=i&&i<=e[j].back().second));}}for(i=1;i<=n;i++){ans=(ans+f[t&1][i])%p;}cout<<ans<<endl;fclose(stdin);fclose(stdout);return 0; }
-
又有 \(\sum\limits_{j=1}^{n}f_{i,j}=\sum\limits_{j=1}^{n}f_{i-1,j} \times \sum\limits_{j=1}^{n}check(i,j)\) ,所以可以只记录第 \(i\) 天不拥挤的城市个数来进行转移。时间复杂度仍为 \(O(nV)\) ,但常数有所减小,可以获得 \(60pts\) 。
点击查看代码
const ll p=1000000007; vector<pair<ll,ll> >e[5010]; bool cmp(pair<ll,ll> a,pair<ll,ll> b) {return a.first>b.first; } int main() {freopen("travel.in","r",stdin);freopen("travel.out","w",stdout);ll n,m,s,t,x,l,r,ans=0,cnt=0,i,j,k;cin>>n>>m>>s>>t;t-=s;for(i=1;i<=m;i++){cin>>x>>l>>r;l-=s;r-=s;e[x].push_back(make_pair(l,r));}for(i=1;i<=n;i++){sort(e[i].begin(),e[i].end(),cmp);ans+=(!(e[i].empty()==0&&e[i].back().first<=0&&0<=e[i].back().second));}for(i=1;i<=t;i++){cnt=0;for(j=1;j<=n;j++){while(e[j].empty()==0&&e[j].back().second<i){e[j].pop_back();}cnt+=(!(e[j].empty()==0&&e[j].back().first<=i&&i<=e[j].back().second));}ans=cnt*ans%p;}cout<<ans<<endl;fclose(stdin);fclose(stdout);return 0; }
-
现在问题来到了怎么快速求出某一天不拥挤的城市个数。而某一天拥挤的城市个数可以通过线段树区间加、单点查询实现,空间复杂度为 \(O(m \log V)\) ,时间复杂度为 \(O((m+V) \log V)\) ,可以获得 \(70pts\) 。
点击查看代码
const ll p=1000000007; struct SMT {int root=0,rt_sum=0;struct SegmentTree{int ls,rs,lazy;}tree[10000010];#define lson(rt) (tree[rt].ls)#define rson(rt) (tree[rt].rs)int build_rt(){rt_sum++;lson(rt_sum)=rson(rt_sum)=tree[rt_sum].lazy=0;return rt_sum;}void update(int &rt,int l,int r,int x,int y,int val){rt=(rt==0)?build_rt():rt;if(x<=l&&r<=y){tree[rt].lazy+=val;return;}int mid=(l+r)/2;if(x<=mid){update(lson(rt),l,mid,x,y,val);}if(y>mid){update(rson(rt),mid+1,r,x,y,val);}}int query(int rt,int l,int r,int pos){if(rt==0){return 0;}if(l==r){return tree[rt].lazy;}int mid=(l+r)/2;if(pos<=mid){return query(lson(rt),l,mid,pos)+tree[rt].lazy;}else{return query(rson(rt),mid+1,r,pos)+tree[rt].lazy;}} }T; int main() {freopen("travel.in","r",stdin);freopen("travel.out","w",stdout);int n,m,s,t,x,l,r,i;ll ans=1;scanf("%d%d%d%d",&n,&m,&s,&t);t-=s;T.update(T.root,0,t,0,t,n);for(i=1;i<=m;i++){scanf("%d%d%d",&x,&l,&r);l-=s;r-=s;T.update(T.root,0,t,l,r,-1);}for(i=0;i<=t;i++){ans=ans*T.query(T.root,0,t,i)%p;}printf("%lld\n",ans);fclose(stdin);fclose(stdout);return 0; }
-
线段树难以维护区间乘积,考虑低配版珂朵莉树(没有推平操作)维护上述过程,可以获得 \(80pts\) 。
点击查看代码
const ll p=1000000007; ll qpow(ll a,ll b,ll p) {ll ans=1;while(b){if(b&1){ans=ans*a%p;}b>>=1;a=a*a%p;}return ans; } struct ODT {struct node{ll l,r;mutable ll col;bool operator < (const node &another) const{return l<another.l;}};set<node>s;void init(ll n,ll t){s.insert((node){0,t,n});}set<node>::iterator split(ll pos){set<node>::iterator it=s.lower_bound((node){pos,0,0});if(it!=s.end()&&it->l==pos){return it;}it--;if(it->r<pos){return s.end();}ll l=it->l,r=it->r,col=it->col;s.erase(it);s.insert((node){l,pos-1,col});return s.insert((node){pos,r,col}).first;}void update(ll l,ll r,ll val){set<node>::iterator itr=split(r+1),itl=split(l);for(set<node>::iterator it=itl;it!=itr;it++){it->col+=val;}}ll query(ll l,ll r){set<node>::iterator itr=split(r+1),itl=split(l);ll ans=1;for(set<node>::iterator it=itl;it!=itr;it++){ans=ans*qpow(it->col,it->r-it->l+1,p)%p;}return ans;} }D; int main() {freopen("travel.in","r",stdin);freopen("travel.out","w",stdout);ll n,m,s,t,x,l,r,i;scanf("%lld%lld%lld%lld",&n,&m,&s,&t);t-=s;D.init(n,t);for(i=1;i<=m;i++){scanf("%lld%lld%lld",&x,&l,&r);l-=s;r-=s;D.update(l,r,-1);}printf("%lld\n",D.query(0,t));fclose(stdin);fclose(stdout);return 0; }
-
- \(45 \%\) :
-
正解
- 先对所有时间进行离散化。
- 类似珂朵莉树考虑维护一段时间内不拥挤城市不变的时间段对答案的影响。
- 具体地,因为是静态修改所以考虑差分,接着双指针维护不拥挤城市即可。
点击查看代码
const ll p=1000000007; ll a[2000010],d[2000010]; struct node {ll x,tim,val; }e[2000010]; bool cmp(node a,node b) {return (a.tim==b.tim)?(a.val<b.val):(a.tim<b.tim); } ll qpow(ll a,ll b,ll p) {ll ans=1;while(b){if(b&1){ans=ans*a%p;}b>>=1;a=a*a%p;}return ans; } int main() {freopen("travel.in","r",stdin);freopen("travel.out","w",stdout);ll n,m,s,t,x,l,r,ans=1,num,i;scanf("%lld%lld%lld%lld",&n,&m,&s,&t);num=n;a[1]=s;a[2]=t+1;for(i=1;i<=m;i++){scanf("%lld%lld%lld",&x,&l,&r);e[2*i-1]=(node){x,l,1};e[2*i]=(node){x,r+1,-1};a[2*i+1]=l;a[2*i+2]=r+1;}sort(e+1,e+1+2*m,cmp);sort(a+1,a+1+2+2*m);a[0]=unique(a+1,a+1+2+2*m)-(a+1);for(l=r=1;r<=a[0]-1;r++){for(;l<=2*m&&e[l].tim<=a[r];l++){if(e[l].val==1){num-=(d[e[l].x]==0);}if(e[l].val==-1){num+=(d[e[l].x]==1);}d[e[l].x]+=e[l].val;}ans=ans*qpow(num,a[r+1]-a[r],p)%p;} printf("%lld\n",ans);fclose(stdin);fclose(stdout);return 0; }
\(T3\) C. 线段树(segment) \(100pts\)
- 详见 NOIP2024模拟2 T3 GHzoj 3733. 线段树 。
\(T4\) D. 量子隧穿问题(experiment) \(14/5pts\)
-
部分分
-
\(14/5pts\) :爆搜。
点击查看代码
const ll p=1000000007; int b[5010],vis[5010],tmp[5010],ans=0; char s[5010]; void dfs(int pos,int n) {if(pos==n+1){for(int i=1;i<=n;i++){tmp[i]=vis[i];}for(int i=1;i<=n;i++){if(tmp[i]==1){if(tmp[b[i]]==0){tmp[i]=0;tmp[b[i]]=1;}}}ans=(ans+tmp[n])%p;return;}else{if(s[pos]=='C'){vis[pos]=1;dfs(pos+1,n);}if(s[pos]=='.'){vis[pos]=0;dfs(pos+1,n);}if(s[pos]=='?'){vis[pos]=1;dfs(pos+1,n);vis[pos]=0;dfs(pos+1,n);}} } int main() {freopen("experiment.in","r",stdin);freopen("experiment.out","w",stdout);int t,n,i,j;scanf("%d",&t);for(j=1;j<=t;j++){ans=0;scanf("%d%s",&n,s+1);for(i=1;i<=n;i++){scanf("%d",&b[i]);}dfs(1,n);printf("%d\n",ans);}fclose(stdin);fclose(stdout);return 0; }
-
-
正解
- 考虑将计数 \(DP\) 转化为概率 \(DP\) ,最后再乘上 \(2^{k}\) 。
- 从 \(i\) 向 \(b_{i}\) 连一条有向边,于是就得到了一个基环树森林。每棵基环树彼此间是互不相关的,故可以只考虑包含盒子 \(n\) 的基环树。
- 设 \(f_{i,j}\) 表示狗当前跑到盒子 \(i\) 时盒子 \(j\) 有猫的概率。
- 先考虑如果是棵树怎么处理。对于树上一条边 \(u \to v\) ,有 \(\begin{cases} f_{u,u}=f_{u-1,u} \times f_{u-1,v} \\ f_{u,v}=f_{u-1,v}+f_{u-1,u} \times (1-f_{u-1,v}) \end{cases}\) ,边界为 \(f_{0,i}=[s_{i}=C]+[s_{i}=?] \times \frac{1}{2}\) 。
- 而到基环树上状态的转移之间具有后效性,因为 \(n\) 过大所以无法高斯消元。
- 对于基环树的环上第一条边(起点编号最小的边) \(u \to v\) ,不妨钦定 \(u,v\) 有/无猫,然后就能当成树来做了。
- 之所以取第一条边是为了保证 \(u\) 已经更新完与之相连的环外的节点。
- 因为直接按照树来做的话,当 \(u\) 更新 \(v\) 时没有计算 \(u\) 在环上的父亲对 \(u\) 的贡献,所以需要钦定后再按照树来做。
点击查看代码
const ll p=1000000007; struct node {ll nxt,to; }e[10010]; ll head[10010],to[10010],vis[10010],f[10010],cnt=0; char s[10010]; void add(ll u,ll v) {cnt++;e[cnt].nxt=head[u];e[cnt].to=v;head[u]=cnt; } ll qpow(ll a,ll b,ll p) {ll ans=1;while(b){if(b&1){ans=ans*a%p;}b>>=1;a=a*a%p;}return ans; } struct DSU {ll fa[100010];void init(ll n){for(ll i=1;i<=n;i++){fa[i]=i;}}ll find(ll x){return (fa[x]==x)?x:fa[x]=find(fa[x]);}void merge(ll x,ll y){x=find(x);y=find(y);if(x!=y){fa[x]=y;}} }D; ll dfs_huan(ll x) {vis[x]=1;return (vis[to[x]]==1)?x:dfs_huan(to[x]); } void dfs(ll x,ll &rt) {rt=min(rt,x);vis[x]=1;if(vis[to[x]]==0){dfs(to[x],rt);} } int main() {freopen("experiment.in","r",stdin);freopen("experiment.out","w",stdout);ll t,n,ans,sum,num,inv=qpow(2,p-2,p),rt,tmp,i,j,k,h;cin>>t;for(h=1;h<=t;h++){cnt=ans=sum=0;memset(e,0,sizeof(e));memset(head,0,sizeof(head));memset(vis,0,sizeof(vis));cin>>n>>(s+1);D.init(n);for(i=1;i<=n;i++){cin>>to[i];add(i,to[i]);D.merge(i,to[i]);sum+=(s[i]=='?');}for(i=1;i<=n;i++){if(D.find(i)==D.fa[n]){rt=dfs_huan(i);break;}}memset(vis,0,sizeof(vis));dfs(rt,rt);for(j=0;j<=1;j++){for(k=0;k<=1;k++){for(i=1;i<=n;i++){if(s[i]=='C'){f[i]=1;}if(s[i]=='.'){f[i]=0;}if(s[i]=='?'){f[i]=inv;}}for(i=1;i<=n;i++){if(i==rt){num=(j==1?f[i]:(1-f[i]+p)%p)*(k==1?f[to[i]]:(1-f[to[i]]+p)%p)%p;f[i]=j;f[to[i]]=k;}tmp=f[i];f[i]=f[i]*f[to[i]]%p;f[to[i]]=(f[to[i]]+((1-f[to[i]]+p)%p)*tmp%p)%p;}ans=(ans+num*f[n]%p)%p;}}cout<<ans*qpow(2,sum,p)%p<<endl;}fclose(stdin);fclose(stdout);return 0; }
总结
- \(T2\) 没想到离散化去优化线段树的空间,而且对于静态区间修改没有马上想到差分可以处理,和 普及模拟3 T2 红黑树 犯了一样的问题。
后记
- \(T3\) 被出在了以前和 \(GXYZ\) 一起打的模拟赛里, \(miaomiao\) 说不用管,正常做就行。