炸雷(详细解析)

文章描述了一个扫雷游戏的关卡,玩家需要通过发射排雷火箭来引爆炸雷。文章提供了两种解题方法,首先是暴力算法,然后是使用哈希算法的解决方案,后者能够更有效地处理大量数据并避免超时。哈希算法通过坐标信息快速定位和处理炸雷,实现高效搜索和引爆过程。
摘要由CSDN通过智能技术生成

 

问题描述

小明最近迷上了一款名为《扫雷》的游戏。其中有一个关卡的任务如下, 在一个二维平面上放置着 nn 个炸雷, 第 ii 个炸雷 \left(x_{i}, y_{i}, r_{i}\right)(xi​,yi​,ri​) 表示在坐标 \left(x_{i}, y_{i}\right)(xi​,yi​) 处 存在一个炸雷, 它的爆炸范围是以半径为 r_{i}ri​ 的一个圆。

为了顺利通过这片土地, 需要玩家进行排雷。玩家可以发射 mm 个排雷火 箭, 小明已经规划好了每个排雷火箭的发射方向, 第 jj 个排雷火箭 \left(x_{j}, y_{j}, r_{j}\right)(xj​,yj​,rj​) 表 示这个排雷火箭将会在 \left(x_{j}, y_{j}\right)(xj​,yj​) 处爆炸, 它的爆炸范围是以半径为 r_{j}rj​ 的一个圆, 在其爆炸范围内的炸雷会被引爆。同时, 当炸雷被引爆时, 在其爆炸范围内的 炸雷也会被引爆。现在小明想知道他这次共引爆了几颗炸雷?

你可以把炸雷和排雷火箭都视为平面上的一个点。一个点处可以存在多个 炸雷和排雷火箭。当炸雷位于爆炸范围的边界上时也会被引爆。

输入格式

输入的第一行包含两个整数 n 、 mn、m.

接下来的 nn 行, 每行三个整数 x_{i}, y_{i}, r_{i}xi​,yi​,ri​, 表示一个炸雷的信息。

再接下来的 mm 行, 每行三个整数 x_{j}, y_{j}, r_{j}xj​,yj​,rj​, 表示一个排雷火箭的信息。

输出格式

输出一个整数表示答案。

样例输入

2 1
2 2 4
4 4 2
0 0 5

样例输出

2

样例说明

示例图如下,排雷火箭 1 覆盖了炸雷 1,所以炸雷 1 被排除;炸雷 1 又覆盖了炸雷 2,所以炸雷 2 也被排除。

评测用例规模与约定

对于 40 %40 的评测用例: 0 \leq x, y \leq 10^{9}, 0 \leq n, m \leq 10^{3}, 1 \leq r \leq 100≤x,y≤109,0≤n,m≤103,1≤r≤10.

对于 100 %100 的评测用例: 0 \leq x, y \leq 10^{9}, 0 \leq n, m \leq 5 \times 10^{4}, 1 \leq r \leq 100≤x,y≤109,0≤n,m≤5×104,1≤r≤10.

运行限制

  • 最大运行时间:1s
  • 最大运行内存: 256M

解题过程

一开始的暴力算法40分

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int  main()
{     int a[10000][10]={0};//雷
int b[10000][10]={0};//排雷

	  int n,m,count=0;
	   scanf("%d%d",&n,&m);
	   for(int i=1;i<=n;i++)
	   {
	   	for(int j=1;j<=3;j++)
	   	{
		   	scanf("%d",&a[i][j]);
		   }
	   }
		
for(int i=1;i<=m;i++)
	   {
	   	for(int j=1;j<=3;j++)
	   	{
		   	scanf("%d",&b[i][j]);
		   }
	   }
	   
	 for(int k=1;k<=m;k++)  
	 {
	 	for(int i=1;i<=n;i++)//这个需要每一个雷区的认真排查,你把循环逻辑想清楚,不要靠记忆!
		 	   {
					if(a[i][4]!=1)
		 	   {
//						if((a[i][1]-b[i][1])*(a[i][1]-b[i][1])+(a[i][2]-b[i][2])*(a[i][2]-b[i][2])<=(b[k][3]*b[k][3]))
                      if((a[i][1]-b[k][1])*(a[i][1]-b[k][1])+(a[i][2]-b[k][2])*(a[i][2]-b[k][2])<=(b[k][3]*b[k][3]))
                      //对应排雷和它的半径想好再写
							 	   	{
											count++;
											a[i][4]=1;//标记
										}
				}
		 	   
		 	   }
	 }
	 
	 //自爆
	          for(int i=1;i<=n;i++)//不认真分析乱写全错,用定一动一法则
	          {
			  		for(int j=i;j<n;j++)//单个循环无法满足全程遍历,应该用双循环,不需要等于N,不然会溢出,因为下面是i+1
			  		//又开始条件乱写
				  	 		 	   {//是i不是j!
				  	 		 	   	if(a[i][4]!=a[j+1][4])//没爆和都爆了都不需要考虑了,而不相等就肯定可能使得另外一个爆
				  	 		 	   	{
										   	if((a[i][1]-a[j+1][1])*(a[i][1]-a[j+1][1])+(a[i][2]-a[j+1][2])*(a[i][2]-a[j+1][2])<=a[i][3]*a[i][3]||(a[i][1]-a[j+1][1])*(a[i][1]-a[j+1][1])+(a[i][2]-a[j+1][2])*(a[i][2]-a[j+1][2])<=a[j+1][3]*a[j+1][3])
											   		 	   	//你看等于号又不写了,拜托关键步骤认真一点行不行?
																   {
											   						count++;
                                     a[i][4]=1;
                                     a[j+1][4]=1;//标记记录已爆
											   						
											   					}
										   	
										   }
				  	 		 	   
				  	 		 	   }
			  }
	 	 
	 	 
	 	 printf("%d",count);

	return 0;
}
//40分,暴力算法
//你的条件设的很多,时间不是问题,应该是爆了空间
//越界出问题了.思路清晰值得赞赏,但是这里就是要解决这个数组问题
//你当然可以开多个数组实现强行放置,但显然不见得高明
//最重要的还是能不能有效利用数组空间--用到哈希算法
//当然,可以开多个数组用两重循环逐一遍历,if条件判断到达后自动换使用另一个数组继承下一次的遍历
//但超不超时不好说.反正遍历效率肯定比哈希要慢

 

哈希AC解题过程

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;//要;
//c++用typedef
ll x, y, r;//写在全局一点问题都没,
//定义的结构体还是函数算重开的,不会有冲突。

int n, m;
const int N = 50010, M = 999997;//使用M的时候要固定用const!
const ll Z = (1e9 + 1);
ll h[M];
ll id[M];
bool st[M];

struct circle
{
    ll x, y, r;
}cir[N];//炸雷50000就够了

ll get_v(ll x, ll y)
{
    // return x*(1e9+1)+y;err
    //数字爆了要加ll,或者自己开一个变量
    //加ll必须是一串数字,所以说要这么写就要写成变量
    return x * Z + y;
}
ll find(ll x, ll y)
{
    ll key = get_v(x, y);
    ll t = (key % M + M) % M;

    // while(h[t]!=0&&h[t]!=key)初始化写的-1
    while (h[t] != -1 && h[t] != key)
    {
        if (++t == M)
            t = 0;
    }

    return t;


}
ll sqr(ll x)
{
    return x * x;
}

// ll dfs(ll x,ll y,ll r)err

void dfs(ll x, ll y, ll r)
//这里只是标记数组,不需要返回类型
{
    st[find(x, y)] = true;

    for (ll i = x - r; i <= x + r; i++)
        for (ll j = y - r; j <= y + r; j++)
            //又把j写成i可以啊你,又开始飘了,又调了半天青光眼
        {
            if (sqr(i - x) + sqr(j - y) <= sqr(r))
            {
                ll t = find(i, j);
                if (id[t] && !st[t])
                {
                    dfs(i, j, cir[id[t]].r);
                }


            }
        }


}
int main()
{

    cin >> n >> m;
    // meset(h,-1,sizeof h);
    memset(h, -1, sizeof h);
    // for(int i=0;i<n;i++)err
    //不能从0开始,以后最好就从1,因为0很可能跟其他数组冲突,
    //比如没有赋值的数组id,后面的程序容易都出错,所以说复杂程序
    //最好都使用1作开头,省去判断
    for (ll i = 1; i <= n; i++)
    {
        // cin>>cir[i];err
        //先插入x,y,r.再放进数组中
        cin >> x >> y >> r;

        cir[i] = { x,y,r };

        ll t = find(x, y);

        if (h[t] == -1)
        {
            h[t] = get_v(x, y);
        }

        if (!id[t] || cir[id[t]].r < r)
        {
            id[t] = i;//如果i从0开始,这样就相当于没有赋值。
        }




    }

    while (m--)
    {

        cin >> x >> y >> r;//火箭的坐标直接扔了是吧?



        for (ll i = x - r; i <= x + r; i++)
            for (ll j = y - r; j <= y + r; j++)
                //又把j写成i可以啊你,又开始飘了,又调了半天青光眼
            {
                if (sqr(i - x) + sqr(j - y) <= sqr(r))
                {
                    // int t=find(x,y);
                    //是i和j不是x,y!写代码麻烦带脑子
                    ll t = find(i, j);
                    if (id[t] && !st[t])
                        //id直接判断是否真值不是!=0
                    {
                        dfs(i, j, cir[id[t]].r);
                    }


                }
            }
    }

    int res = 0;
    for (ll i = 1; i <= n; i++)
    {
        if (st[find(cir[i].x, cir[i].y)])
            res++;
    }

    cout << res;
    return 0;
}

//以后总是超时记得看下循环条件,尤其注意i和j!
//long long不会成为超时的理由,相反ll虽然内存比int 大,但是比int快很多,而且内存大的才一点点
//题目中的内存永远是足够大的
//所以如果想开ll,就随便开,没必要可以判断,反而时间还更短了!
//怕溢出就干脆写多点ll

详解

//核心是哈希
//哈希算法就相当于把没有规律的值,通过他们的一些信息储存在数组中
// 当然处理的手法必须保证下标不会冲突重复
//然后一旦要用到这个数组的某个数据,输入相关信息,再从哈希函数中找就能找到对应下标然后使用了.
//就是使用数据本身的一些特征信息来建立桥梁,每次访问下标就通过访问函数即可获取.

//因为题目的每一个数据都是对应下标产生的,当你想要通过范围寻找坐标判断对应的点的时候
//你又很难找到这个点究竟在哪个数据中
//如果直接无脑插入数组,每一次的检测判断所有的地雷1e5,地雷与地雷,地雷和排雷,必超时
//所以说必须要用哈希直接对应坐标快速查找才能实现AC
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 50010, M = 999997;//十倍的N效果最好,而且质数更好3,7//哈希表长度
//防总是冲突要设大十倍最理想,但是总要比你无脑建立数组来的时间短,再怎么存我存1e5的数据也只用1e6

int n, m;
struct Circle
{
    int x, y, r;
}cir[N];//炸雷
//坐标最好建立结构体统一最好

LL h[M];
//哈希数组:存放哈希值,相当于标记该位置已经被占用,当前下标不可用,是为find找下标服务的
// 同时也是作为返回该下标的标志,即哈希值相同时直接返回这个坐标实现快速查找

// 哈希的核心是找下标

//哈希数组有什么优势吗?相比顺序存储
// 它可以提供非常快速的插入-删除-查找操作
// 无论多少数据,插入和删除需要接近常量的时间:即O(1)的时间级。实际上,只需要几个机器指令即可完成。
// 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素
// 哈希表相对于树来说编码要容易很多
//但就是没有顺序
int id[M];// id数组为哈希数组中cir对应的哈希地雷下标
bool st[M];// 判断是否访问过

LL get_key(int x, int y)
//求哈希值:返回每一个不同坐标的唯一结果
{
    return x * 1000000001ll + y;
    //在1e9+1进制上
    // xy:       第二位        第一位


        //如,1,0/0,1/,不同坐标必须有一个操作确定唯一的值
        //核心就是不要让x,y的不同坐标得出相同的答案.
        //只要在数字后加类型就能直接附上类型,这么写防止溢出

        //最大上限是10^9,
        //这么写是为了确定唯一性
        //为何一定要这么大,小了时间更高?
        //必须是这么写,改小了超时


        //哈希值:xy总体看成一个(1e9 + 1)进制数字,
        //可以将(x,y)看作是一个数字,即xy,因为x,y最大为10^9,所以只要看成看成一个(1e9 + 1)进制数字
        //就可以完美的固定x,y的结果,一定有唯一值,因为y永远不会进位,x永远在y的前面,所以结果永远不同.
        //这样即使y为最大的10^9,它同样无法进位,一直在第一位上,而第二位也是一样,每次满1e9+1进1位,
        //同样第二位最终也是10^9无法到达第三位,但已经确定了每个x,y对应的唯一值,
        //如果进制低了,y就可以进位,这样就没有办法肯定x,y一定有唯一值,答案就不准确了
        //这么写就确保y一直在第一位,x一直在第二位.形象于1e9+1进制的数字xy.y永远在后面,
        //就确定了xy的唯一结果
        //唯一确定的位置,即每次返回的结果不同,这是重点

        //当然最低限度是范围+1,你可以任意选择这以上的任何进制作为你的哈希值,
        //只要x,y能分开.

        //总结:根据数字范围确定进制分割x,y!确定唯一值的核心在于让一个数位上的值永远无法进位即可.
        //不进位就不会有多个值.






}

int find(int x, int y)//寻找哈希值
//快速查找的根源在于与x,y坐标建立联系
//我们很清楚坐标的大小,但是我们却不清楚它具体储存在数组的位置
//所以通过x,y与数组位置联系起来,我们就能很轻松的访问想要的数组
//而不是一个个遍历
{
    LL key = get_key(x, y);//值的大小
    int t = (key % M + M) % M;
    //给所求的哈希值一个存放的位置.


//而设大数组是防止总是冲突
   //这么存是尽可能均匀
    //%M是为了找在数组中的哪一个位置对应存放
   //+M%M是防止是负数的转正操作(因为没有负数下标)

 // 如果该位置已经被占用并且不等于我们要求的哈希值,要在之后找到相应位置
    while (h[t] != -1 && h[t] != key)

        if (++t == M)//遍历+尽头时从头走过
            t = 0;
    //判断该位置是否有赋值过,防止冲突
//有赋值过并且不是自己想要的结果key的
//我们就往下找,直到找到结果key为所需下标

//函数的两用:有就要找看看是否哈希值对应,
//没有赋值表示没有冲突,我直接把x,y对应到这个t下标(最理想的结果)


    return t;//返回存放下标

}

int sqr(int x)
{
    return x * x;
}

void dfs(int x, int y, int r)
{
    st[find(x, y)] = true;//标记引爆的炸雷

    for (int i = x - r; i <= x + r; i++)
        for (int j = y - r; j <= y + r; j++)
            if (sqr(i - x) + sqr(j - y) <= sqr(r))
            {
                int t = find(i, j);
                if (id[t] && !st[t])
                    dfs(i, j, cir[id[t]].r);
            }
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    //初始化哈希数组
    for (int i = 1; i <= n; i++)//炸雷
    {
        int x, y, r;
        scanf("%d%d%d", &x, &y, &r);
        cir[i] = { x, y, r };
        //这个数组是为了最后输出结果遍历使用的
        //是地雷总数,没有被哈希处理过的原始数据
          //作用, 存储所有数据
         // 因为可能有同坐标地雷的存在,而标记法只对一个数据生效,所以说必须要开一个数组来记录所有数据
            // 如果没有同坐标,那么完全就不用cir数组
   
         
        int t = find(x, y);//找对应x,y的哈希值的数组下标
        if (h[t] == -1)
            h[t] = get_key(x, y);//哈希值的存放到h[t]
            //哈希数组就是为了存放炸雷的坐标 
            //好处,不用浪费太多的数组空间,
            //达成什么目的?可以用数组干嘛啊哈希?
            //数组设太大会超时,而且也不允许设这么大


        if (!id[t] || cir[id[t]].r < r)
            //初始没有赋值的数组一定是假的

            // id数组没有被标记或者找到了同一个坐标点更大半径的地雷
            //(就挺阴间的,同坐标还可能有多个雷,三个阴间测评数据)
            id[t] = i;
        //地雷是随机插入的,
        //就是利用哈希来确定所需地雷的位置
        //所以id的作用就是用哈希对应地雷的下标.
        //插入哈希炸雷+更换为最大的一个雷


    }
    //统一使用t下标就是为了将这些数据同时调用,不用查找
    //相当于一条线将所有需要用的数据串联起来
    //一旦我求出了这个t,那么这些值我都能直接拿起来用,

    while (m--)//火箭,每一种火箭的遍历
    {
        int x, y, r;
        scanf("%d%d%d", &x, &y, &r);

        for (int i = x - r; i <= x + r; i++)
            // 从(x-r, y-r)枚举到(x+r, y+r),圆不好枚举,枚举矩形
            for (int j = y - r; j <= y + r; j++)
                if (sqr(i - x) + sqr(j - y) <= sqr(r))//枚举火箭范围中有没有炸雷
                    //表示坐标在圆内
                    //(x- a)²+(y-b)²=r²
                    //圆的方程不是面积!
                    // 表示坐标为(a,b)的圆心
                    // 你不熟悉可以改循环为x,y,但是与题目设置冲突不太好
                    // 你套公式就对了
                   //枚举多一点没什么大碍
                {
                    int t = find(i, j);
                    //寻找哈希位置下标,直接调用所有数据

                    //?//当前坐标的导入
                    //寻找哈希值
                    //像这里,我们可以直接根据坐标来很快锁定对应数据的位置
                    //然后直接导出对应位置,比你直接插入数组无法根据特征判断位置输出
                    //想要的结果快多了

                    if (id[t] && !st[t])
                        // 如果该点有地雷,没有被访问过,能被炸到
                      //这里是顺着题目思路写下去的,火箭引爆炸雷

                      //炸雷接连引爆其他炸雷,深度搜索。
                        dfs(i, j, cir[id[t]].r);

                }

    }

    int res = 0;
    for (int i = 1; i <= n; i++)
        if (st[find(cir[i].x, cir[i].y)])//炸雷总数
            //寻找所有的雷中炸了的
            
        //输入cir时输入进了所有的炸雷,包括同坐标的。
        //前面替换了哈希的炸雷也没关系,因为只要是同坐标被标记
        //那么每次遍历到相同的坐标时也一样是为真值继续加一,
        //这就是为什么要替换成最大半径,
        //只要这个最大范围的炸雷都炸了,那在里面的炸雷也同样会炸,
        // 而且只有最大范围的炸雷才是准确的炸雷与扎雷的搜索
        //可以直接重复加入同坐标这样的一个操作.
            res++;

    printf("%d\n", res);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值