Akane-Weekly #3NOIP 十连测(3)

Akane-Weekly #3:NOIP 十连测(3)

有质量的比赛。

B.

简要题意:

有一个 \([1,n]\) 的排列,每次有三种操作:

  1. 整个序列向左循环移动一位,
  2. 整个序列向右循环移动一位,
  3. \([1,m]\) 中选一位,把数加入 \(b\) 数组里面。

求至少进行多少次操作 \(1,2\) ,使数组 \(b\) 恰好为 \(1\)\(n\)

Solution:

发现一个贪心,最优解满足每一步都要最优。

考虑 \(dp\) 转移, \(dp[i][0/1]\) 表示当前取到第 \(i\) 个数,取完了之后窗口左/右侧在 \(a[i]\) 的位置上时,最小的方案。

但这样不是最优的,因为有这样一种情况:前面的 \(i\) 转移完之后,后面可能又连续的一些数,满足正好在 \(i\) 的窗口里,不用移动。

考虑每得到 \(dp\) 状态之后往右推,直接转移到下一个不在窗口中的数。

考虑预处理 “不在窗口中的数”,可以值域线段树维护,详见代码。

线段树存区间最小的不在当前窗口的数,时间复杂度 \(O(n \log n)\)

Code:

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
int n,m,a[1000010];
int sum[2000010],indl[500010];
int qd[500010][2];
long long fdy[500010];
long long dp[500010][2];// 0:start 1:end
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0',ch=getchar();
	}
	return x*f;
}
void pushup(int id){
	sum[id]=min(sum[id<<1],sum[id<<1|1]);
}
void build(int id,int l,int r){
	if(l==r){
		sum[id]=n+1;
		return;
	}
	int mid=(l+r)>>1;
	build(id<<1,l,mid);
	build(id<<1|1,mid+1,r);
	pushup(id);
}
void update(int id,int l,int r,int x,int v){
	if(l==r){
		indl[l]=v;
		if(indl[l]>0){
			sum[id]=l;
		}else{
			sum[id]=n+1;
		}
	}else{
		int mid=(l+r)>>1;
		if(x<=mid) update(id<<1,l,mid,x,v);
		else update(id<<1|1,mid+1,r,x,v);
		pushup(id); 
	}
}
int cur,curp;
void query(int id,int l,int r,int x,int y){
	if(x>y||l>r){
		return;
	}
	if(x<=l&&r<=y){
		cur=min(cur,sum[id]);
		return;
	}
	int mid=(l+r)>>1;
	if(x<=mid) query(id<<1,l,mid,x,y);
	if(y>mid) query(id<<1|1,mid+1,r,x,y);
}
int pre;
signed main(){
	n=read(),m=read();
	if(n==m){
		printf("0");
		return 0;
	}
	for(int i=1;i<=n;i++) a[i]=read(),a[i+n]=a[i],fdy[a[i]]=i;
	build(1,1,n);
	for(int i=m+1;i<=n;i++) update(1,1,n,a[i],1);
	int l,r;
	for(int i=m;i<=n+m-1;i++){
		l=a[i-m+1],r=n,cur=n+1;
		query(1,1,n,l,r);
		qd[a[i-m+1]][0]=cur-1;
		update(1,1,n,a[i-m+1],1),update(1,1,n,a[i+1],0);
	}
	build(1,1,n);
	for(int i=m+1;i<=n;i++) update(1,1,n,a[i],1);
	for(int i=m;i<=n+m-1;i++){
		l=a[i],r=n,cur=n+1;
		query(1,1,n,l,r);
		qd[a[i]][1]=cur-1;
		update(1,1,n,a[i-m+1],1),update(1,1,n,a[i+1],0);
	}
	memset(dp,0x3f,sizeof dp);
	for(int i=1;i<=n;i++){
		if(fdy[i]>m){
			pre=i;
			break;
		}
	}
	long long inf=dp[0][0];
	long long ans=inf;
	long long sm,bg,tp;
	dp[pre][0]=min(fdy[pre]-1,1+n-fdy[pre]);
	dp[pre][1]=min(fdy[pre]-m,m+n-fdy[pre]);
	for(int i=1;i<=n;i++){
		if(dp[i][0]!=inf){
			if(qd[i][0]==n) ans=min(ans,dp[i][0]);
			else{
				sm=min(fdy[qd[i][0]+1],fdy[i]);
				bg=max(fdy[qd[i][0]+1],fdy[i]);
				dp[qd[i][0]+1][0]=min(dp[qd[i][0]+1][0],min(dp[i][0]+bg-sm,dp[i][0]+sm+n-bg));
				tp=fdy[qd[i][0]+1]-m+1;
				if(tp<=0) tp+=n;
				sm=min(tp,fdy[i]);
				bg=max(tp,fdy[i]);
				dp[qd[i][0]+1][1]=min(dp[qd[i][0]+1][1],min(dp[i][0]+bg-sm,dp[i][0]+sm+n-bg));
			}
		}
		if(dp[i][1]!=inf){
			if(qd[i][1]==n) ans=min(ans,dp[i][1]);
			else{
				sm=min(fdy[qd[i][1]+1],fdy[i]);
				bg=max(fdy[qd[i][1]+1],fdy[i]);
				dp[qd[i][1]+1][1]=min(dp[qd[i][1]+1][1],min(dp[i][1]+bg-sm,dp[i][1]+sm+n-bg));
				tp=fdy[qd[i][1]+1]+m-1;
				if(tp>n) tp-=n;
				sm=min(tp,fdy[i]);
				bg=max(tp,fdy[i]);
				dp[qd[i][1]+1][0]=min(dp[qd[i][1]+1][0],min(dp[i][1]+bg-sm,dp[i][1]+sm+n-bg));
			}
		}
	}
	ans=min(ans,min(dp[n][0],dp[n][1]));
	printf("%lld\n",ans);


C.

最神仙的一题,推了一整节音乐课。

简要题意:

给你一个有向图,让你求 \(1-n-1\) 的最短路径,重复边路径的费用只算一次。

\(1 \le n \le 200\)

Solution:(80 pts)

考虑我们最后会走哪些路径。

发现会张成这个样子:

黑色的是我们去的时候走的路径,红色的边是逐步往上跳,然后重复经过蓝色的边(免费)。

我们可以根据这个设计出一种比较暴力的算法。

\(dp[u][v]\) ,表示我们构造了一条 \(1-v\) 的黑色路径,下一级红色的边将跳到 \(u\) 然后回来的时候将从 \(v\) 像上一级的点跳的最小花费。

转移也不难写,设从 \(dp[u][v]\) 转移至 \(dp[i][j]\) ,有:

\[dp[i][j]=\min(dp[i][j],dp[u][v]+dis[v][i]+dis[i][j]+dis[j][u]) \]

如图,黑色的是 \(dp[u][v]\) 包含的边,红色和绿色的是加的边。

那么怎么进行 \(dp\) 的过程呢,我们可以用类似 \(dijkstra\) 的思路,每次取最小的作为 \(u,v\) 进行更新。

预处理 \(dis\) ,则可以在 \(O(n^4\log n)\) 时间内解决问题,结合特殊性质分可以得到 $80 $ pts.

Solution:(100 pts)

为啥刚刚那个能得 80?因为它离正解很近了。

考虑转移的过程,一次枚举两个点 \(i,j\) 太麻烦了,考虑只枚举一个点 \(i\) 。现在考虑怎么利用它来更新状态。

我们首先可以更新到 \(dp[u][i]\) ,方程为 \(dp[u][i]=\min(dp[u][i],dp[u][v]+dis[v][i])\) ,因为从下面转上来的点还在 \(u\) ,我们只需要修改一下状态,把往上转的点为 \(v\) 改成为 \(u,v\) 中的某个点即可。这样的话就可以这样转移了。

我们还可以更新 \(dp[v][i]\) ,这个很直接,加上 \((v,i),(i,u)\) 即可实现。

\(dp[v][i]=min(dp[v][i],dp[u][v]+dis[v][i]+dis[i][u])\)

时间复杂度 \(O(n^3\log n)\)

#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int dp[220][220];
int ans;
bool vis[220][220];
struct node{
	int w,u,v;
	bool operator <(const node &a) const{
		return a.w<w;
	}
};
priority_queue<node> pq;
inline int read(){
	int x=0,f=1;
	char ch;
	ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0',ch=getchar();
	}
	return x*f;
}
int dis[220][220]; 
void Floyd(){
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
			}
		}
	}
}
int x;
signed main(){
	n=read();
	memset(dis,0x3f,sizeof dis);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			x=read();
			if(i==j||x!=0) dis[i][j]=x;
		}
	}
	Floyd();
	memset(dp,0x3f,sizeof dp);
	ans=dp[1][1];
	dp[1][1]=0;
	pq.push(node{0,1,1});
	while(!pq.empty()){
		node tmp=pq.top();
		pq.pop();
		int u=tmp.u,v=tmp.v;
		if(vis[u][v]==1) continue;
		vis[u][v]=1;
		for(int i=1;i<=n;i++){
			if(dp[v][i]>dis[v][i]+dis[i][u]+dp[u][v]){
				dp[v][i]=dis[v][i]+dis[i][u]+dp[u][v];
				pq.push(node{dp[v][i],v,i});
			}
			if(dp[u][i]>dis[v][i]+dp[u][v]){
				dp[u][i]=dis[v][i]+dp[u][v];
				pq.push(node{dp[u][i],u,i});
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++) ans=min(ans,dp[i][j]+dis[j][n]+dis[n][i]);
	}
	cout<<ans<<endl;
}

D.

不太难。

简要题意:

给你一个左右各 \(n\) 个点的二部图,让你连不多于 \(m\) 条边,使得对于 \(\forall k \in [1,n]\) 该二部图最大匹配为 \(k\) 时权值和的最大值。

对于一个二部图,它的权值定义为 \(\sum a[i][deg_i]\)\(deg_i\) 为点 \(i\) 的度数。

\(n \le 30,m\le 60\)

Solution:

一开始我们的思路是爆搜,每次一条一条的加边,为了判断最大匹配正好为 \(n\) ,我们在判断时会用到给点染色。

将每一条匹配边中选恰好一个点加入点覆盖中,只有在点覆盖的点能够连非匹配边。

所以我们的点有四种:

1.点覆盖中的点

2.匹配边上的不在点覆盖中的点

3.非匹配边上的不在点覆盖中的点

4.没有连边的点。

边也有三种:

1.匹配边

2.连接两个 \(2\) 类点的边

3.连接一个 \(1\) 类点一个 \(3\) 类点的边。

我们发现可以将二部图的左右部分开考虑。

设我们做出了左部里面总共连了 \(u_0\) 条边,有 \(v_0\) 条左部点是 \(2\) 类点的非匹配边,\(j_0\) 条匹配边,\(k_0\) 个二类点的最大权值和,右部类似。

我们发现当

\[u_0=u_1 \le m, j_0=j_1=k,k_o+k_1=k,v_0+v_1>=u_0 \]

时即可。

(最后一条是大于因为一些边可能左右都是 \(2\) 类点,要容斥掉)。

那我们直接两个部分分开 \(dp\) 即可,要记得滚动数组。

dp[cur][j][k][u+d][v]=max(dp[cur][j][k][u+d][v],dp[pre][j][k][u][v]+a[i][d]);

if(d) dp[cur][j+1][k][u+d][v]=max(dp[cur][j+1][k][u+d][v],dp[pre][j][k][u][v]+a[i][d]);
																						if(d) dp[cur][j+1][k+1][u+d][v+d]=max(dp[cur][j+1][k+1][u+d][v+d],dp[pre][j][k][u][v]+a[i][d]);

时间复杂度 \(O(n^3m^3)\) ,可以通过。

Code:

#include<bits/stdc++.h>
using namespace std;
int a[110][110];
int dp[2][35][35][65][65];//
int dp2[2][35][35][65][65];
int n,m;
inline int read(){
	int x=0,f=1;
	char ch;
	ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0',ch=getchar();
	}
	return x*f;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=2*n;i++){
		for(int j=0;j<=m;j++){
			a[i][j]=read();
		}
	}
	memset(dp,~0x3f,sizeof dp);
	memset(dp2,~0x3f,sizeof dp2);
	dp[0][0][0][0][0]=0,dp2[0][0][0][0][0]=0;
	for(int i=1;i<=n;i++){
		int cur=i&1,pre=cur^1;
		memset(dp[cur],~0x3f,sizeof dp[cur]);
		for(int j=0;j<=i;j++){
			for(int k=0;k<=j;k++){
				for(int u=j;u<=m;u++){
					for(int v=0;v<=u;v++){
						for(int d=0;d<=m-u;d++){
							//匹配边选一个覆盖,覆盖点才能连非匹配边。 
							//d 第 i 个点增加的。
							//u 前面总共连得边 
							//k 前面的在匹配边上的覆盖的点的个数
							//j 前面连的匹配边
							//v 前面的在费匹配边覆盖的点的个数 
							dp[cur][j][k][u+d][v]=max(dp[cur][j][k][u+d][v],dp[pre][j][k][u][v]+a[i][d]);
							if(d) dp[cur][j+1][k][u+d][v]=max(dp[cur][j+1][k][u+d][v],dp[pre][j][k][u][v]+a[i][d]);
							if(d) dp[cur][j+1][k+1][u+d][v+d]=max(dp[cur][j+1][k+1][u+d][v+d],dp[pre][j][k][u][v]+a[i][d]);
						}
					}
				}
			}
		}
	}
	for(int i=1;i<=n;i++){
		int cur=i&1,pre=cur^1;
		memset(dp2[cur],~0x3f,sizeof dp2[cur]);
		for(int j=0;j<=i;j++){
			for(int k=0;k<=j;k++){
				for(int u=j;u<=m;u++){
					for(int v=0;v<=u;v++){
						for(int d=0;d<=m-u;d++){
							//匹配边选一个覆盖,覆盖点才能连非匹配边。 
							//d 第 i 个点增加的。
							//u 前面总共连得边 
							//k 前面的在匹配边上的覆盖的点的个数
							//j 前面连的匹配边
							//v 前面的在非匹配边以覆盖点为开始的个数。 
							dp2[cur][j][k][u+d][v]=max(dp2[cur][j][k][u+d][v],dp2[pre][j][k][u][v]+a[i+n][d]);
							if(d) dp2[cur][j+1][k][u+d][v]=max(dp2[cur][j+1][k][u+d][v],dp2[pre][j][k][u][v]+a[i+n][d]);
							if(d) dp2[cur][j+1][k+1][u+d][v+d]=max(dp2[cur][j+1][k+1][u+d][v+d],dp2[pre][j][k][u][v]+a[i+n][d]);
						}
					}
				}
			}
		}
	}
	int ans=0;
	for(int k=1;k<=n;k++){
		ans=0;
		for(int i=0;i<=k;i++){
			for(int e=0;e<=m;e++){
				for(int x=0;x<=m;x++){
					for(int y=0;y<=m;y++){
						if(e<=x+y){
							if(dp[n&1][k][i][e][x]>=0&&dp2[n&1][k][k-i][e][y]>=0){
								ans=max(ans,dp[n&1][k][i][e][x]+dp2[n&1][k][k-i][e][y]);
							}
						}
					}
				}
			}
		}
		printf("%d ",ans);
	}
}

end

posted @ 2023-09-14 20:52  ArizonaYYDS  阅读(29)  评论(0)    收藏  举报