【模拟赛】纪中提高A组 19.8.9 测试

Posted on 2019-08-15 20:28  opethrax  阅读(616)  评论(0编辑  收藏  举报

Task.1 走格子

题目大意:C和F在一个可以看作 \(N\times M\) 的矩阵的房间中,矩阵中的每一个元素可以是:

  1. 障碍:"#"

  2. C或者F的起点:"C"或"F"

  3. 空区域:"."

C携带了一把传送枪,每次C都可以:

  1. 花费一个单位时间移动到相邻的空区域

  2. 不花费时间向上下左右之一的方向的墙壁上发射传送门(传送门最多只能同时存在两扇,如果已经存在两扇再发射一扇那最早出现的那扇会消失,一个位置不能存在两扇传送门)

  3. 花费一个单位时间进入相邻墙壁上的传送门中移动到另一个传送门前方的格子中。

求C到F的最短时间。

数据范围:\(1\leq N,M\leq 500\),保证地图最外围是障碍。

https://store.steampowered.com/app/400/Portal/

上来我们就想想爆搜?能过吗?可以拿到85pts的好成绩???

正确的操作是这样的:当你在某个位置时,你可以向四个方向上发射两个传送门,一定可以走到那个最近的墙边进传送门到另外一面墙前面。

做法就很简单了:bfs或最短路算法,考虑一下上述情况就可以了。

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<utility>
using namespace std;

template<class T>void read(T &x){
	x=0; char c=getchar();
	while(c<'0'||'9'<c)c=getchar();
	while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef pair<int,int> pr;
const int N=505;

int n,m,s,t;
char mp[N][N];
vector<pr>e[N*N];
int num[N][N],L[N][N],R[N][N],U[N][N],D[N][N];
int dis[N*N];
priority_queue<pr>q;
bool vis[N*N];
int dx[5]={0,0,1,-1};
int dy[5]={1,-1,0,0};

void add(int x,int y,int w){e[x].push_back(pr(y,w));}
bool dijkstra(){
	memset(vis,0,sizeof(vis));
	memset(dis,0x7f,sizeof(dis));
	dis[s]=0; q.push(pr(0,s));
	pr now; int x,y,d;
	while(!q.empty()){
		now=q.top(); q.pop();
		x=now.second; if(vis[x])continue; vis[x]=1;
		for(int i=e[x].size()-1;~i;i--){
			y=e[x][i].first; d=e[x][i].second;
			if(dis[y]>dis[x]+d){
				dis[y]=dis[x]+d;
				if(!vis[y])q.push(pr(-dis[y],y));
			}
		}
	}
	return dis[t]!=dis[0];
}
int main(){
	freopen("cell.in","r",stdin);
	freopen("cell.out","w",stdout);
	read(n); read(m); int cnt=0;
	for(int i=1;i<=n;i++){
		scanf("%s",mp[i]+1);
		for(int j=1;j<=m;j++){
			if(mp[i][j]=='#'){L[i][j]=R[i][j]=U[i][j]=D[i][j]=-1; continue;}
			num[i][j]=++cnt;
			if(mp[i][j]=='C') s=num[i][j];
			if(mp[i][j]=='F') t=num[i][j];
			L[i][j]=L[i][j-1]+1; U[i][j]=U[i-1][j]+1;
		}
	}
	for(int i=n;i;i--) for(int j=m;j;j--) if(mp[i][j]!='#'){R[i][j]=R[i][j+1]+1; D[i][j]=D[i+1][j]+1;}
	int nx,ny,d;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++) if(mp[i][j]!='#'){
			for(int k=0;k<4;k++){
				nx=i+dx[k]; ny=j+dy[k];
				if(mp[nx][ny]=='#')continue;
				add(num[i][j],num[nx][ny],1);
			}
			d=min(min(L[i][j],R[i][j]),min(U[i][j],D[i][j]))+1;
			add(num[i][j],num[i][j-L[i][j]],d);
			add(num[i][j],num[i][j+R[i][j]],d);
			add(num[i][j],num[i-U[i][j]][j],d);
			add(num[i][j],num[i+D[i][j]][j],d);
		}
	}
	if(dijkstra()) printf("%d\n",dis[t]);
	else puts("wtf");
	return 0;
}

Task.2 扭动的树

题目大意:有一棵 \(key\) 为键值 \(val\) 为权值 \(n\) 个点的二叉查找树,定义某个节点的 \(sum\) 值为它的子树内的 \(val\) 的和。告诉你 \(n\) 个节点的 \(key\) 值和 \(val\) 值,求满足树上任意一条边两个端点 \(key\) 值最大公约数不为 \(1\) 时,树上所有节点的 \(sum\) 值的和最大是多少。

数据范围:\(1\leq N\leq 300,1\leq key_i\leq 10^18,1\leq val_i\leq 10^6\)

一棵二叉搜索树的中序遍历就是所有键值排序后的结果。试着排序后的 \(n\) 个元素进行合并,可以使用类似区间DP的思路,定义状态 \(f_{i,j,k}\) 表示区间 \([i,j]\) 合并成一棵子树根为 \(k\) 的最大答案,转移类似区间DP,复杂度 \(O(n^4)\)

对这个思路进行优化:区间 \([i,j]\)\(k\) 为根的情况最终一定可以由 \([i,k-1]\)\([k+1,j]\) 得到。所以我们试着把上述DP的第三维改进一下,定义 \(f_{i,j,0/1}\) 表示区间 \([i,j]\)\(i-1\)\(j+1\) 为根的答案,转移依然是类似区间DP的,枚举根 \(k\),复杂度 \(O(N^3)\)

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<utility>
using namespace std;
template<class T>void read(T &x){
	x=0; char c=getchar();
	while(c<'0'||'9'<c)c=getchar();
	while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef long long ll;
typedef pair<ll,ll> pr;
const int N=305;
#define key first
#define val second
int n;
pr a[N];
ll sum[N],f[N][N][2],ans;
bool e[N][N];
void cmax(ll &x,ll y){if(x<y)x=y;}
ll gcd(ll x,ll y){return y==0?x:gcd(y,x%y);}
int main(){
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
	read(n);
	for(int i=1;i<=n;i++){read(a[i].key); read(a[i].val);}
	sort(a+1,a+n+1);
	if(a[1].key==1){puts("-1"); return 0;}
	memset(f,-0x3f,sizeof(f));
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i].val;
		for(int j=i+1;j<=n;j++)
			e[i][j]=(gcd(a[i].key,a[j].key)!=1);
		if(e[i-1][i]) f[i][i][0]=a[i].val;
		if(e[i][i+1]) f[i][i][1]=a[i].val;
	}
	for(int len=2;len<=n;len++)
		for(int i=1,j=i+len-1;j<=n;i++,j++){
			ll cost=sum[j]-sum[i-1];
			for(int k=i;k<=j;k++){
				ll add=(i<k)*f[i][k-1][1]+(k<j)*f[k+1][j][0]+cost;
				if(e[i-1][k]) cmax(f[i][j][0],add);
				if(e[k][j+1]) cmax(f[i][j][1],add);
				if(len==n) cmax(ans,add);
			}
		}
	printf("%lld\n",ans);
	return 0;
}

Task.3 旋转字段

题目大意:定义一个排列的价值是排列中 \(p_i=i\) 这样的固定点的个数。现在和以选择一个区间 \([L,R]\) 翻转,求翻转后最多的固定点。

数据范围:\(1\leq N\leq 10^5\)

有这么一个性质:在位置 \(i\) 上的数字 \(x\) 关于 \(\frac{(i+x)}{2}\) 翻折会产生 \(1\) 的贡献,并且某个位置上的数它会产生贡献的对称点都是固定的。再考虑一下最后答案翻转的区间长什么样子:区间左右端点至少有一个关于对称点(区间中点)翻转后会产生贡献,否则如果选了这两个端点一定不会是答案变优。

根据上述的分析,我们想到了一个贪心做法:把相同对称点的点放在一起做,从小到达枚举区间半径,利用前缀和差分得到翻转区间之外的答案,复杂度 \(O(N^2)\) 。还不够好,我们发现枚举半径时有很多多余的情况,把这些情况去掉,每次由近至远枚举区间端点,跳过翻转后不产生贡献的点,计算答案方式和上面相同。这样每个位置只被考虑一次,复杂度 \(O(N)\)

代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;

template<class T>void read(T &x){
	x=0; char c=getchar();
	while(c<'0'||'9'<c)c=getchar();
	while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef pair<int,int> pr;
void cmax(int &x,int y){if(x<y)x=y;}
void cmin(int &x,int y){if(x>y)x=y;}
const int N=100050;
int n,a[N],pre[N],ans;
vector<int>q[N<<1];
int main(){
	freopen("rotate.in","r",stdin);
	freopen("rotate.out","w",stdout);
	read(n);
	for(int i=1;i<=n;i++){
		read(a[i]); pre[i]=pre[i-1]+(a[i]==i);
		if(a[i]>i) q[a[i]+i].push_back(a[i]);
		else q[a[i]+i].push_back(i);
	}
	int L,R;
	for(int i=(n<<1);i>1;i--){
		sort(q[i].begin(),q[i].end());
		for(int j=0;j<q[i].size();j++){
			R=q[i][j]; L=i-R;
			cmax(ans,pre[L-1]+pre[n]-pre[R]+j+1);
		}
	}
	printf("%d\n",ans);
	return 0;
}