2017年天梯赛全国总决赛题集 L3-3 森森美图 (30 分)(计算几何,BFS,DFS)

描述:

森森最近想让自己的朋友圈熠熠生辉,所以他决定自己写个美化照片的软件,并起名为森森美图。众所周知,在合照中美化自己的面部而不美化合照者的面部是让自己占据朋友圈高点的绝好方法,因此森森美图里当然得有这个功能。 这个功能的第一步是将自己的面部选中。森森首先计算出了一个图像中所有像素点与周围点的相似程度的分数,分数越低表示某个像素点越“像”一个轮廓边缘上的点。 森森认为,任意连续像素点的得分之和越低,表示它们组成的曲线和轮廓边缘的重合程度越高。为了选择出一个完整的面部,森森决定让用户选择面部上的两个像素点A和B,则连接这两个点的直线就将图像分为两部分,然后在这两部分中分别寻找一条从A到B且与轮廓重合程度最高的曲线,就可以拼出用户的面部了。 然而森森计算出来得分矩阵后,突然发现自己不知道怎么找到这两条曲线了,你能帮森森当上朋友圈的小王子吗?

为了解题方便,我们做出以下补充说明:

  • 图像的左上角是坐标原点(0,0),我们假设所有像素按矩阵格式排列,其坐标均为非负整数(即横轴向右为正,纵轴向下为正)。
  • 忽略正好位于连接A和B的直线(注意不是线段)上的像素点,即不认为这部分像素点在任何一个划分部分上,因此曲线也不能经过这部分像素点。
  • 曲线是八连通的(即任一像素点可与其周围的8个像素连通),但为了计算准确,某像素连接对角相邻的斜向像素时,得分额外增加两个像素分数和的√​2​​​倍减一。例如样例中,经过坐标为(3,1)和(4,2)的两个像素点的曲线,其得分应该是这两个像素点的分数和(2+2),再加上额外的(2+2)乘以(√​2​​​−1),即约为5.66。

输入格式:

输入在第一行给出两个正整数N和M(5≤N,M≤100),表示像素得分矩阵的行数和列数。

接下来N行,每行M个不大于1000的非负整数,即为像素点的分值。

最后一行给出用户选择的起始和结束像素点的坐标(X​start​​,Y​start​​)和(X​end​​,Y​end​​)。4个整数用空格分隔。

输出格式:

在一行中输出划分图片后找到的轮廓曲线的得分和,保留小数点后两位。注意起点和终点的得分不要重复计算。

输入样例:

6 6
9 0 1 9 9 9
9 9 1 2 2 9
9 9 2 0 2 9
9 9 1 1 2 9
9 9 3 3 1 1
9 9 9 9 9 9
2 1 5 4

输出样例:

27.04

题意:

给定一个得分矩阵,在给两个点的坐标,这两个点连成的直线将这个矩阵划分成两个部分,要你在两个区域内分别求出一条从起点到终点的点权和最小的路径,并输出两条路径的点权和之和(如果路径中两个点是斜向相连的,那么点权和需要额外增加(\sqrt{2} - 1)倍的两点点权之和,另外起点和终点只计算一次)

思路:

简单的区域限制的搜索问题

不过这里的限制不是单点障碍,而是一条直线,通过向量(叉积)可以快速判断一个点与直线的位置关系

再看搜索,如果用DFS,由于路径是八联通的,所以复杂度来到了O(n^{2 * 8}),n<=100,则必然会超时的(DFS得分21分,在最后会附上)

既然DFS会超时,那么果断用BFS

但要注意的是,我们求的是点权和最小,而不是路径步数最小,也就是说直接用普通的BFS去求的话可能得不到正确答案,因为可能多走几步点权和会小不少(比如将一次斜向行走改为走两步直向的,普通BFS得分3分)

所以说,我们就不能简单的用visited数组去标记一个点是否被访问过,因为同一个点可能会被更新多次,可能会被重复加进队列里面

所以我们需要去存下从起点到当前点的点权和是多少,在加入队列之前就判断一下,此次更新是否会比之前的点权和更小(注意这里不能写等号,这样会导致同一个点搜索多次,但答案却得不到更新,而且大大增加了搜索范围,会使得一个点超时,一个点超内存,得分24分)

判断点与直线位置

定义:平面上三个点A(x1,y1)、B(x2,y2)、C(x3,y3),判断点C与向量AB的位置关系。
在这里插入图片描述

S ( A , B , C ) = ∣ y1 , y2 , y3 ∣ = ( x1 − x3 ) ( y2 − y3 ) − ( y1 − y3 ) ( x2 − x3 ) / 2

若S(A,B,C)大于0,则C在向量AB的左侧;
若S(A,B,C)小于0,则C在向量AB的右侧;
若S(A,B,C)等于0,则C在向量AB上。

由于要进行两次搜索,所以把搜索函数封装好,那么我们每次搜索的时候,就传入flag的值(1或-1),在判断点是否在指定区域内,只满足该点计算出的面积量与flag是同号即可

还有一个坑,就是输入的时候"即横轴向右为正,纵轴向下为正",这里和数组的储存方式是对称的,但我们只要在输入起点终点坐标反过来输入就行了

2021/4/21:2021天梯模拟赛更新:

哈希优化队列,接着之前说的一个点可能会入队多次,如果在队列里有重复的点的话,那么只有对最后一个点去拓展才是有意义的,因为它是当前的最小值,更新在它之前的点并不能使得答案变小,还徒增时间和空间消耗。具体做法是开一个计数数组,可以开二维也可以开一维(这里我开的是一维,相当于把每一个下一行都拼接在上一行的末尾),这个数组用来统计当前队列里某一个点出现的次数,每次入队的时候加一,出队的时候减一,并且只有在计数值等于0,也就是除了自身队列里没有与之相同的点了,才对其进行拓展。(此项优化,时间快了十倍,空间减小一半)

代码一(BFS):

//L3-1 森森美图 (30 分)
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm> 
using namespace std;
const int N = 110 , INF = 1e9;

struct Point
{
	int x;
	int y;
	double score;
};

int n , m;
int G[N][N];
int st[N * N];
double cur_score[N][N];
int fx , fy , ex , ey;
int dx[8][2] = {{-1,0},{1,0},{0,1},{0,-1},{-1,1},{1,-1},{-1,-1},{1,1}};

int pos(int x , int y)
{
	//A(fx , fy)  B(ex , ey)  C(x , y)
	return (fx - x) * (ey - y) - (fy - y) * (ex - x);
}

//判断该点是否越界,是否更新后会变小,是否满足在直线给定的一侧(由于终点在直线上所以需要特判)
bool check(int x , int y , int flag , double score)
{
	if(x < 0 || x >= n || y < 0 || y >= m || cur_score[x][y] <= score)
		return false;
	return pos(x , y) * flag > 0 || (x == ex && y == ey);
}

double bfs(int flag)
{
	queue<Point> q;
	q.push({fx , fy , G[fx][fy]});
	memset(st , 0 , sizeof st);
	st[fx * n + fy] = 1;
	
	double ans = INF;
	for(int i = 0 ; i < N ; i++)	//将当前点分数置为正无穷
		for(int j = 0 ; j < N ; j++)
			cur_score[i][j] = INF;
			
	while(q.size())
	{
		Point t = q.front();
		q.pop();
		
		if(--st[t.x * n + t.y] > 0)		//哈希优化,只拓展队列中最后一个重复点 
			continue;
		
		if(t.x == ex && t.y == ey)
		{
			ans = min(ans , t.score);	//由于每个点会更新多次,所以每次走到终点都取最小值
			continue;
		}
		
		for(int i = 0 ; i < 8 ; i++)
		{
			int tx = t.x + dx[i][0] , ty = t.y + dx[i][1];
			
			double s = G[tx][ty];
			if(i >= 4)				   //如果是斜线,需要额外增加点权和
				s += (sqrt(2) - 1) * (G[tx][ty] + G[t.x][t.y]);
			if(check(tx , ty , flag , t.score + s))
			{
				st[tx * n + ty]++;
				q.push({tx , ty , t.score + s});
				cur_score[tx][ty] = t.score + s;
			}
		}
	}
	
	return ans;
}

int main()
{
	cin>>n>>m;
	for(int i = 0 ; i < n ; i++)
		for(int j = 0 ; j < m ; j++)
			cin>>G[i][j];
	cin>>fy>>fx>>ey>>ex;		//交换输入横纵坐标 
	printf("%.2f\n" , bfs(1) + bfs(-1) - G[fx][fy] - G[ex][ey]);	//需要减去重复的起点和终点点权 
	return 0; 
}

代码二(DFS 21分):

//L3-3 森森美图 (30 分)
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;

struct Point
{
	int x;
	int y;
	double score;
};

int n, m;
int G[N][N];
bool vis[N][N];
int fx, fy, ex, ey;
int flag = 1;
double ans = 1e9;
int dx[8][2] = {{1,0},{-1,0},{0,-1},{0,1},{-1,-1},{1,-1},{-1,1},{1,1}};

int pos(int x , int y)
{
	//A(fx , fy)  B(ex , ey)  C(x , y)
	return (fx - x) * (ey - y) - (fy - y) * (ex - x); 
}

//判断该点是否越界,是否更新后会变小,是否满足在直线给定的一侧(由于终点在直线上所以需要特判)
bool check(int x , int y)
{
	if(x < 0 || x >= n || y < 0 || y >= m || vis[x][y])
		return false;
	return pos(x , y) * flag > 0 || (x == ex && y == ey);
}

void dfs(int tx , int ty , double ts)
{
//	cout<<tx<<" "<<ty<<endl;
	if(ts >= ans)
		return ;
	if(tx == ex && ty == ey)
	{
		ans = ts;
		//cout<<"ans:"<<ans<<endl;
		return ;
	}
	
	for(int i = 0 ; i < 8 ; i++)
	{
		int x = tx + dx[i][0] , y = ty + dx[i][1];
		if(check(x , y))
		{
			double s = G[x][y];
			if(i >= 4)
				s += (G[tx][ty] + G[x][y]) * (sqrt(2) - 1);
			
			vis[x][y] = true;
			dfs(x , y , ts + s);
			vis[x][y] = false;
		}
	}
}

int main()
{
	scanf("%d %d" , &n , &m);
	for(int i = 0 ; i < n ; i++)
		for(int j = 0 ; j < m ; j++)
			scanf("%d" , &G[i][j]);
	scanf("%d %d %d %d" , &fy , &fx , &ey , &ex);
	
	
	vis[fx][fy] = true;
	dfs(fx , fy , G[fx][fy]);
    double t = ans;
    ans = 1e9 , flag = -1;
	vis[fx][fy] = true;
	dfs(fx , fy , G[fx][fy]);
	
	printf("%.2lf\n" , t + ans - G[fx][fy] - G[ex][ey]);
	return 0;
}

 

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值