NOIP2024加赛1

NOIP2024加赛1

\(T1\) HZTG2080. 玩游戏 \(24pts\)

  • 强化版: HDU6326 Problem H. Monster Hunter

  • 部分分

    • \(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;
    }
    
  • 正解

    • 挂下官方题解的做法。

    • 考虑扫描线后线段树维护纵轴。对于同一个纵坐标只保留最靠后的点(横坐标最靠后的)一定更优。

    • 先分别以 \(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\)
posted @ 2024-11-03 19:31  hzoi_Shadow  阅读(64)  评论(5编辑  收藏  举报
扩大
缩小