NOIP2024加赛1
NOIP2024加赛1
\(T1\) HZTG2080. 玩游戏 \(24pts\)
-
部分分
- \(24pts\) : \(O(n^{2})\) 枚举所有可能的状态进行转移。
点击查看代码
ll a[100010],sum[100010],f[2][100010]; int main() { ll t,n,k,i,j,len,l,r; cin>>t; for(j=1;j<=t;j++) { cin>>n>>k; for(i=1;i<=n;i++) { cin>>a[i]; sum[i]=sum[i-1]+a[i]; } memset(f,0,sizeof(f)); f[1][k]=1; if(sum[n]-sum[1]<=0) { for(len=2;len<=n;len++) { for(l=1,r=l+len-1;r<=n;l++,r++) { f[len&1][l]=((f[(len-1)&1][l+1]|f[(len-1)&1][l])&(sum[r]-sum[l]<=0)); } } if(f[n&1][1]==0) { cout<<"No"<<endl; } else { cout<<"Yes"<<endl; } } else { cout<<"No"<<endl; } } return 0; }
-
正解
- 将 \(k\) 左边和右边各看出一个序列(左边的同时进行翻转),再分别求一次前缀和。
- 此时问题等价于对于 \(c_{1 \sim m_{1}},d_{1 \sim m_{2}}\) 各有一个指针 \(l,r=0\) ,每次可以令 \(l\) 或 \(r\) 增加 \(1\) ,且需要保证任意时刻有 \(c_{l}+d_{r} \le 0\) ,询问是否能把 \(l,r\) 同时移到末尾。
- 反悔贪心暴力跳过去发现不合法后再跳回来的时间复杂度不太对。
- 考虑分别找到 \(l,r\) 后面第一个更小的位置,如果能把哪个指针移过去就移过去,若两个指针都移动不了了显然是
No
。但是这样的话,最后 \(l,r\) 就会停在数组中最小值的位置,后面的移动不是很容易处理。 - 考虑钦定能走到最后的位置,然后再类似上述正着跑的思路倒着跑一遍即可。此时如果有解则一定覆盖到了有解时的状态。
点击查看代码
ll a[100010],c[100010],d[100010],nxtc[100010],nxtd[100010],maxc[100010],maxd[100010]; void init1(ll c[],ll nxt[],ll maxc[]) { for(ll last=1,i=2;i<=c[0];i++) { if(c[last]>=c[i]) { nxt[last]=i; last=i; } else { maxc[last]=max(maxc[last],c[i]); } } } void init2(ll c[],ll nxt[],ll maxc[]) { for(ll last=c[0],i=c[0]-1;i>=1;i--) { if(c[last]>c[i]) { nxt[last]=i; last=i; } else { maxc[last]=max(maxc[last],c[i]); } } } bool check(ll l,ll r) { while(nxtc[l]!=0||nxtd[r]!=0) { if(nxtc[l]!=0) { if(maxc[l]+d[r]>0) { if(nxtd[r]==0||c[l]+maxd[r]>0) { return false; } r=nxtd[r]; } else { l=nxtc[l]; } } else { if(nxtd[r]==0||c[l]+maxd[r]>0) { return false; } r=nxtd[r]; } } return true; } int main() { ll t,n,k,i,j; scanf("%lld",&t); for(j=1;j<=t;j++) { scanf("%lld%lld",&n,&k); c[0]=d[0]=1; c[1]=d[1]=0; memset(nxtc,0,sizeof(nxtc)); memset(nxtd,0,sizeof(nxtd)); memset(maxc,-0x3f,sizeof(maxc)); memset(maxd,-0x3f,sizeof(maxd)); for(i=1;i<=n;i++) { scanf("%lld",&a[i]); } for(i=k+1;i<=n;i++) { c[0]++; c[c[0]]=a[i]+c[c[0]-1]; } for(i=k;i>=2;i--) { d[0]++; d[d[0]]=a[i]+d[d[0]-1]; } if(c[c[0]]+d[d[0]]<=0) { init1(c,nxtc,maxc); init1(d,nxtd,maxd); if(check(1,1)==false) { printf("No\n"); } else { init2(c,nxtc,maxc); init2(d,nxtd,maxd); if(check(c[0],d[0])==false) { printf("No\n"); } else { printf("Yes\n"); } } } else { printf("No\n"); } } return 0; }
\(T2\) HZTG2081. 排列 \(20pts\)
-
部分分
- \(20 \%\) :生成全排列后模拟。
- 另外 \(30 \%\) :打表可知当 \(k=1\) 时输出 \(2^{n-1}\) 。
- 整个序列的形式一定是类似峰形,被最大值分割成两个左右序列,故最终有 \(\sum\limits_{i=0}^{n-1}\dbinom{n-1}{0}=2^{n-1}\) 即为所求。
点击查看代码
ll a[1010]; set<ll>s; set<ll>::iterator it; queue<ll>q; 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() { ll n,k,p,ans=0,cnt=0,i; cin>>n>>k>>p; for(i=1;i<=n;i++) { a[i]=i; } if(k==1) { cout<<qpow(2,n-1,p)<<endl; } else { do { s.clear(); for(i=1;i<=n;i++) { s.insert(i); } for(cnt=1;cnt<=k;cnt++) { for(it=s.begin();it!=s.end();it++) { if((next(it)!=s.end()&&a[*it]<a[*next(it)])||(it!=s.begin()&&a[*it]<a[*prev(it)])) { q.push(*it); } } while(q.empty()==0) { s.erase(s.find(q.front())); q.pop(); } if(s.size()==1) { break; } } if(s.size()==1&&cnt==k) { ans=(ans+1)%p; } }while(next_permutation(a+1,a+1+n)); cout<<ans<<endl; } return 0; }
-
正解
-
挂下官方题解的笛卡尔树上 \(DP\) 的做法。
-
仍考虑利用整个序列会被最大值分割成左右两个序列,而这两个序列直接互不干扰的优秀性质。
-
观察到本题中 恰好为 \(m\) 可以转化为 至多 \(m,m-1\) 的前缀和作差 。
-
设 \(f_{i,j,0/1,0/1}\) 表示长度为 \(i\) ,至多操作 \(j\) 次后变成一个数,左边界外不挨着/挨着存在大于最大值的数,右边界外不挨着/挨着存在大于最大值的数的区间个数。
- 若不存在则说明是在边界。
-
转移时考虑枚举中转点 \(k\) 并分讨其与两侧区间的最大值的大小关系。
- 若 \(k\) 为整个区间的最大值,有 \(f_{i,j,0,0}=\sum\limits_{k=1}^{i}f_{k-1,j,0,1} \times f_{i-k,j,1,0} \times \dbinom{i-1}{k-1}\) 。
- 若 \(k\) 比右区间的最大值大,比左区间的最大值小,需要操作一次消掉 \(k\) ,有 \(f_{i,j,1,0}=\sum\limits_{k=1}^{i}f_{k-1,j-1,1,1} \times f_{i-k,j,1,0} \times \dbinom{i-1}{k-1}\) 。
- 若 \(k\) 比左区间的最大值大,比右区间的最大值小,需要操作一次消掉 \(k\) ,有 \(f_{i,j,0,1}=\sum\limits_{k=1}^{i}f_{k-1,j,0,1} \times f_{i-k,j-1,1,1} \times \dbinom{i-1}{k-1}\) 。
- 若 \(k\) 比左右区间的最大值都要小,需要左右任意操作一次消掉 \(k\) ,同时容斥掉左右都需要 \(j\) 次才能消完的情况,有 \(f_{i,j,1,1}=\sum\limits_{k=1}^{i}(f_{k-1,j,1,1} \times f_{i-k,j,1,1}-(f_{k-1,j,1,1}-f_{k-1,j-1,1,1}) \times (f_{i-k,j,1,1}-f_{i-k,j-1,1,1})) \times \dbinom{i-1}{k-1}\) 。
-
边界为 \(f_{0,i,0/1,0/1}=1(i \in [0,m])\) 。
-
最终,有 \(f_{n,m,0,0}-f_{n,m-1,0,0}\) 即为所求。
点击查看代码
ll C[1010][1010],f[1010][1010][2][2]; int main() { ll n,m,p,tmp1,tmp2,i,j,k; cin>>n>>m>>p; if(1.0*m>log2(n)+5.0) { cout<<0<<endl; } else { C[0][0]=C[1][0]=C[1][1]=1; for(i=2;i<=n;i++) { C[i][0]=1; for(j=1;j<=i;j++) { C[i][j]=(C[i-1][j-1]+C[i-1][j])%p; } } for(i=0;i<=m;i++) { f[0][i][0][0]=f[0][i][0][1]=f[0][i][1][0]=f[0][i][1][1]=1; } for(i=1;i<=n;i++) { for(j=1;j<=m;j++) { for(k=1;k<=i;k++) { f[i][j][0][0]=(f[i][j][0][0]+(f[k-1][j][0][1]*f[i-k][j][1][0]%p)*C[i-1][k-1]%p)%p; f[i][j][1][0]=(f[i][j][1][0]+(f[k-1][j-1][1][1]*f[i-k][j][1][0]%p)*C[i-1][k-1]%p)%p; f[i][j][0][1]=(f[i][j][0][1]+(f[k-1][j][0][1]*f[i-k][j-1][1][1]%p)*C[i-1][k-1]%p)%p; tmp1=(f[k-1][j][1][1]-f[k-1][j-1][1][1]+p)%p; tmp2=(f[i-k][j][1][1]-f[i-k][j-1][1][1]+p)%p; f[i][j][1][1]=(f[i][j][1][1]+((f[k-1][j][1][1]*f[i-k][j][1][1]%p-tmp1*tmp2%p+p)%p)*C[i-1][k-1]%p)%p; } } } cout<<(f[n][m][0][0]-f[n][m-1][0][0]+p)%p<<endl; } return 0; }
-
\(T3\) HZTG2082. 最短路 \(20pts\)
-
部分分
- \(20pts\) :钦定只能经过哪些点后判断是否能够到达。
点击查看代码
struct node { int nxt,to; }e[70000]; int head[300],vis[300],check[300],a[300],ans=0,cnt=0; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } bool bfs(int s,int t) { memset(vis,0,sizeof(vis)); queue<int>q; q.push(s); vis[s]=1; while(q.empty()==0) { int x=q.front(); q.pop(); for(int i=head[x];i!=0;i=e[i].nxt) { if(vis[e[i].to]==0&&check[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } return vis[t]; } void dfs(int pos,int n,int sum) { if(pos==n+1) { if(bfs(1,n)==true&&bfs(n,1)==true) { ans=min(ans,sum); } } else { check[pos]=0; dfs(pos+1,n,sum+a[pos]); check[pos]=1; dfs(pos+1,n,sum); } } int main() { int n,m,u,v,i; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; ans+=a[i]; } for(i=1;i<=m;i++) { cin>>u>>v; add(u,v); } if(bfs(1,n)==true&&bfs(n,1)==true) { dfs(1,n,0); cout<<ans<<endl; } else { cout<<-1<<endl; } return 0; }
-
正解
- 从 \(n\) 到 \(1\) 的路径一定形如先往回走一段,再沿着从 \(1\) 到 \(n\) 的路径走一段交替出现。
- 称从 \(1\) 到 \(n\) 的路径为向下走的边,从 \(n\) 到 \(1\) 的路径为向上走的边。
- 设 \(f_{i,j}\) 表示向下走时从 \(i\) 到 \(j\) 的最小花费, \(g_{i,j}\) 表示向上走时 从 \(i\) 到 \(j\) 的最小花费。
- 用向上、下走的边可能会更好理解。
- 转移的过程可以用类似最短路进行转移。连边时仍考虑枚举中转点 \(k\) ,判断向上走还是向下走。
- 可以将二维 \(dijkstra\) 压成一维。
点击查看代码
int a[260],d[260][260],dis[150010],vis[150010]; vector<pair<int,int> >e[1500010]; void add(int u,int v,int w) { e[u].push_back(make_pair(v,w)); } int work(int x,int y,int n) { return (x-1)*n+y; } void dijkstra(int s) { memset(dis,0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); priority_queue<pair<int,int> >q; dis[s]=0; q.push(make_pair(-dis[s],s)); while(q.empty()==0) { int x=q.top().second; q.pop(); if(vis[x]==0) { vis[x]=1; for(int i=0;i<e[x].size();i++) { if(dis[e[x][i].first]>dis[x]+e[x][i].second) { dis[e[x][i].first]=dis[x]+e[x][i].second; q.push(make_pair(-dis[e[x][i].first],e[x][i].first)); } } } } } int main() { int n,m,u,v,i,j,k; cin>>n>>m; memset(d,0x3f,sizeof(d)); for(i=1;i<=n;i++) { cin>>a[i]; d[i][i]=0; } for(i=1;i<=m;i++) { cin>>u>>v; d[u][v]=min(d[u][v],a[v]); } for(k=1;k<=n;k++) { for(i=1;i<=n;i++) { if(i!=k&&d[i][k]!=0x3f3f3f3f) { for(j=1;j<=n;j++) { if(i!=j&&j!=k&&d[i][k]!=0x3f3f3f3f&&d[k][j]!=0x3f3f3f3f) { d[i][j]=min(d[i][j],d[i][k]+d[k][j]); } } } } } for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { for(k=1;k<=n;k++) { if(d[j][k]!=0x3f3f3f3f&&j!=k) { add(work(i,j,n),work(k,i,n)+n*n,d[j][k]-a[k]); } if(d[i][k]!=0x3f3f3f3f&&d[k][j]!=0x3f3f3f3f) { add(work(i,j,n)+n*n,work(i,k,n),d[i][k]+d[k][j]); } } } } dijkstra(work(n,n,n)); cout<<((dis[work(1,1,n)]==0x3f3f3f3f)?-1:dis[work(1,1,n)]+a[1])<<endl; return 0; }
\(T4\) HZTG2083. 矩形 \(10pts\)
-
部分分
- \(20pts\) :模拟。
- 第 \(x\) 和第 \(y\) 个矩形相交的矩形为左下角为 \(\max(r_{1,x},r_{1,y}),\max(c_{1,x},c_{1,y})\) ,右上角为 \(\min(r_{2,x},r_{2,y}),\min(c_{2,x},c_{2,y})\) ,画图手摸即可。
点击查看代码
int r1[100010],c1[100010],r2[100010],c2[100010]; struct DSU { int fa[100010]; void init(int n) { for(int i=1;i<=n;i++) { fa[i]=i; } } int find(int x) { return (fa[x]==x)?x:fa[x]=find(fa[x]); } void merge(int x,int y,int &ans) { x=find(x); y=find(y); if(x!=y) { fa[x]=y; ans--; } } }D; bool check(int x,int y) { return (max(r1[x],r1[y])<=min(r2[x],r2[y])&&max(c1[x],c1[y])<=min(c2[x],c2[y])); } int main() { int n,ans,i,j; cin>>n; for(i=1;i<=n;i++) { cin>>r1[i]>>c1[i]>>r2[i]>>c2[i]; } ans=n; D.init(n); for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { if(i!=j&&check(i,j)==true) { D.merge(i,j,ans); } } } cout<<ans<<endl; return 0; }
- \(20pts\) :模拟。
-
正解
-
挂下官方题解的做法。
-
考虑扫描线后线段树维护纵轴。对于同一个纵坐标只保留最靠后的点(横坐标最靠后的)一定更优。
-
先分别以 \(x_{1},x_{2}\) 为第一、二关键字升序排序,然后按照 \(x_{1}\) 进行合并,按照 \(x_{2}\) 修改。需要线段树维护区间时判断是否被推平。
-
时间复杂度均摊后是对的,但我不会分析。
点击查看代码
struct node { int x1,y1,x2,y2; }e[100010]; bool cmp(node a,node b) { return (a.x1==b.x1)?(a.x2<b.x2):(a.x1<b.x1); } struct DSU { int fa[100010]; void init(int n) { for(int i=1;i<=n;i++) { fa[i]=i; } } int find(int x) { return (fa[x]==x)?x:fa[x]=find(fa[x]); } void merge(int x,int y) { x=find(x); y=find(y); if(x!=y) { fa[x]=y; } } }D; struct SMT { struct SegmentTree { int lazy,id,pos,col; }tree[400010]; int lson(int x) { return x*2; } int rson(int x) { return x*2+1; } void pushup(int rt) { if(tree[lson(rt)].col==0||tree[rson(rt)].col==0||tree[lson(rt)].id!=tree[rson(rt)].id) { tree[rt].col=0; } else { tree[rt].id=tree[lson(rt)].id; tree[rt].pos=tree[lson(rt)].pos; tree[rt].col=1; } } void build(int rt,int l,int r) { tree[rt].lazy=tree[rt].id=tree[rt].pos=0; tree[rt].col=1; if(l==r) { return; } int mid=(l+r)/2; build(lson(rt),l,mid); build(rson(rt),mid+1,r); } void pushdown(int rt) { if(tree[rt].lazy!=0) { tree[lson(rt)].id=tree[rson(rt)].id=tree[rt].id; tree[lson(rt)].pos=tree[rson(rt)].pos=tree[rt].pos; tree[lson(rt)].lazy=tree[rson(rt)].lazy=tree[rt].lazy; tree[rt].lazy=0; } } void update(int rt,int l,int r,int x,int y,int pos,int id) { if(x<=l&&r<=y&&tree[rt].col==1) { if(tree[rt].pos<pos) { tree[rt].id=id; tree[rt].pos=pos; tree[rt].lazy=1; } return; } pushdown(rt); int mid=(l+r)/2; if(x<=mid) { update(lson(rt),l,mid,x,y,pos,id); } if(y>mid) { update(rson(rt),mid+1,r,x,y,pos,id); } pushup(rt); } void merge(int rt,int l,int r,int x,int y,int pos,int id) { if(x<=l&&r<=y&&tree[rt].col==1) { if(tree[rt].pos>=pos) { D.merge(tree[rt].id,id); } return; } pushdown(rt); int mid=(l+r)/2; if(x<=mid) { merge(lson(rt),l,mid,x,y,pos,id); } if(y>mid) { merge(rson(rt),mid+1,r,x,y,pos,id); } pushup(rt); } }T; int main() { int n,ans=0,i; cin>>n; for(i=1;i<=n;i++) { cin>>e[i].x1>>e[i].y1>>e[i].x2>>e[i].y2; } sort(e+1,e+1+n,cmp); D.init(n); T.build(1,1,100000); for(i=1;i<=n;i++) { T.merge(1,1,100000,e[i].y1,e[i].y2,e[i].x1,i); T.update(1,1,100000,e[i].y1,e[i].y2,e[i].x2,i); } for(i=1;i<=n;i++) { ans+=(i==D.fa[i]); } cout<<ans<<endl; return 0; }
-
总结
- 貌似成乱搞场了,但我一点乱搞做法没写。
- \(T1\) 因为贪心一眼假了遂没写。
- \(T3\) 因为爆搜加剪枝的复杂度不是很保真,遂没写。
- \(T2\) 先写了 \(k=1\) 的部分分,写完 \(n \le 9\) 后的暴力分后忘验证了,挂了 \(30pts\) 。
- \(T4\) 因为不会判断矩形是否相交,所以挂了 \(10pts\) 。
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18523814,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。