洛谷P10533 [Opoi 2024] 热核武器 (本蒟蒻第一篇题解)

[Opoi 2024] 热核武器

题目背景

跳蚤国与蛐蛐国正在激战!

level

上面是战术核显卡,与题目没有关联。

题目描述

跳蚤国的国土可以看作平面直角坐标系。

跳蚤国有 \(N+1\) 座城市,有 \(1\) 座是首都,位于 \((0,0)\),另 \(N\) 座是普通城市,在这里假设首都为 \(0\) 号城市,其他城市编号为 \(1\)\(N\),对于每一座普通城市,位于 \((x_i,y_i)\)

由于跳蚤国财力有限,对于每一个不是首都的城市 \(i\),它会选择一个城市 \(j\) 修建一条双向公路。令 \(dis(x,y)\)\(x\)\(y\) 城市的欧几里得距离,则对于每一个不是首都的城市 \(i\),它所对应的 \(j\) 则是满足 \(dis(j,0) \le dis(i,0)\)\(j \ne i\) 的所有点中 \(dis(i,j)\) 最小的点,如有多个合法 \(j\),取其中编号最小的一个。

定义一座城市的 \(\gamma\) 值为这个城市走到首都所需要的最小道路数 \(+1\)如果走不到首都,设 \(\gamma\) 值为 \(0\)

蛐蛐国要对跳蚤国进行战术核显卡打击,这次行动分为两个组:洛伦兹组和安培组。每个组都要对跳蚤国的部分城市进行打击,其中两个组需要恰好把跳蚤国每个城市打击一遍。

对于这两个组来说,名利是最重要的,而蛐蛐国的评功标准是按照本次行动所打击城市的 \(\gamma\) 值和。所以你需要求出:有没有一种划分方式使得洛伦兹组和安培组分别的打击城市的 \(\gamma\) 值和相等,可以,输出 Yes,否则输出 No

输入格式

\(1\) 行输入一个整数 \(N\),表示跳蚤国普通城市的数目。

接下来第 \(2 \sim N+1\) 行,第 \(i+1\) 行输入两个整数,表示第 \(i\) 座城市的横纵坐标 \((x_i,y_i)\)

输出格式

一行一个字符串,Yes 或者 No。表示是否会有一种方法使得洛伦兹组和安培组分别的打击城市的 \(\gamma\) 值和相等。

样例 #1

样例输入 #1

4
-1 -1
1 0
1 -2
-2 2

样例输出 #1

Yes

提示

样例解释

这幅图是长这样的:

对于 \(C1\)\(C0\)\(C2\) 满足 \(dis(j,0) \le dis(C1,0)\),但是 \(C0\)\(C1\) 距离更近,添加边 \((C1,C0)\)

对于 \(C2\),只有 \(C0\) 满足 \(dis(j,0) \le dis(C2,0)\),添加边 \((C2,C0)\)

对于 \(C3\)\(C0\)\(C1\)\(C2\) 满足 \(dis(j,0) \le dis(C3,0)\),但是 \(C2\)\(C3\) 距离最近,因此添加边 \((C3,C2)\)注意这里是因为在 \(C3\) 处考虑时,最优点为 \(C2\),所以 \(C3\) 才向 \(C2\) 修建了一条公路,和公路 \((C2,C0)\) 完全独立。

对于 \(C4\),其他所有点都满足 \(dis(j,0) \le dis(C4,0)\),但是 \(C0\)\(C4\) 距离最近,添加边 \((C4,C0)\)

得到下面的表:

城市编号 \(\gamma\)
0 1
1 2
2 2
3 3
4 2

所以把 \(0,1,2\) 分给洛伦兹组,\(3,4\) 分给安培组即可。

数据范围

\(1 \le N \le 500\)\(-10^6 \le x_i,y_i \le 10^6\)

特殊说明

由于本题输出只有 YesNo,所以本题采用最小分值评测法,即取所有测试点的得分最小值作为结果。


题目解答

问题简单分析

  1. 构建图
    • 城市编号从 0(首都)到 N
    • 对于每个普通城市 i(1 ≤ i ≤ N),找到满足 dis(j, 0) ≤ dis(i, 0)j ≠ i 的城市 j 中距离 i 最近的城市。如果有多个,选择编号最小的 j
    • 添加双向公路 (i, j), (j, i)
  2. 计算 γ 值
    • 对于每个城市,计算从首都到该城市的最小道路数 d,则 γ = d + 1
    • 如果无法到达首都,γ = 0
  3. 划分城市
    • 将所有城市划分为俩个组,使得两个组的 γ 值和相等。
    • 判断是否存在这样的划分方式。

解决思路

  1. 构建图

    • 使用邻接表来表示城市之间的连接。
    • 使用 广度优先搜索(BFS) 来计算从首都到每个城市的最短道路数。
  2. 计算 γ 值

    • 初始化所有城市的 γ 值为 0
    • 对于能到达首都的城市,γ 值为最短路径长度加 1
  3. 划分城市并判断可行性

    • 这是一个经典的 划分问题,目标是将 γ 值分为两部分,使得两部分的和相等。
    • 使用 动态规划(DP)(利用位运算优化)来判断是否存在这样的划分。
  4. 特殊考虑

    • 城市的坐标可能较大,因此在计算距离时,使用 长整型(long long 避免整数溢出。
    • 由于传送门数量和城市数量较大,需优化代码以满足时间和空间要求。(利用位运算对动态规划(dp)进行优化)

代码逐步实现:

前置代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;  // long long太长,直接 ll 代替
  1. 城市结构与距离计算
    • 定义了 city 结构体来存储每个城市的坐标。
    • 使用 dist 函数计算两个城市之间的欧几里得距离的平方,避免使用浮点数。
// 定义城市结构,包含坐标
struct city {
    ll x;
    ll y;
};
// 计算两个城市之间的欧几里得距离的平方
ll dist(const city& a, const city& b) {
    ll dx = a.x - b.x;
    ll dy = a.y - b.y;
    return dx * dx + dy * dy;
}
  1. 构建邻接表
    • 对于每个普通城市 i(1 ≤ i ≤ N),遍历所有可能的城市 j(0 ≤ j ≤ N,j ≠ i),找到满足 dis(j,0) <= dis(i,0)j 中距离 i 最近的城市。如果有多个,选择编号最小的 j
    • ij 之间添加双向边。
// 构建邻接表
vector<vector<int>> adj(N + 1, vector<int>());
    
for(int i = 1; i <= N; i ++)
{
    ll min = LLONG_MAX;  // LLONG_MAX = 2 ^ 63 - 1
    int sj = -1;
    for(int j = 0; j <= N; j ++)
    {
        if(j == i) continue;   // 满足 dis(j,0) <= dis(i,0)
        if(cap[j] <= cap[i])
        {
            ll ij = dist(cities[i], cities[j]);
            if(ij < min)
            {
                min = ij;
                sj = j;
            }
            else if(ij == min && j < sj)
                sj = j;
        }
    }
    if(sj != -1)
    {
        adj[i].push_back(sj);
        adj[sj].push_back(i);
    }
}
  1. 广度优先搜索(BFS)计算最短路径
    • 从首都(城市 0)开始进行 BFS,计算每个城市到首都的最短道路数。
    • distance_bfs[i] 存储城市 i 到首都的最短道路数,未访问的城市保持 -1
// BFS 从首都 (0) 开始,计算最短路径
vector<int> dist_bfs(N+1, -1);   // -1 表示未访问
queue<int> q;
q.push(0);
dist_bfs[0] = 0;
    
while(!q.empty())
{
    int u = q.front(); q.pop();
    for(auto &v: adj[u])
        if(dist_bfs[v] == -1)
        {
            dist_bfs[v] = dist_bfs[u] + 1;
            q.push(v);
        }
}
  1. 计算 γ 值

    • 对于每个城市 i,如果 dist_bfs[i] 不等于 -1,则 γ[i] = dist_bfs[i] + 1;否则,γ[i] = 0

    • 将所有 γ 值大于 0 的城市存入 gv 向量。

  2. 动态规划实现:

    • 初始化 dp 为 0,表示初始子集和为 0。
    • 对于每个 γ 值 g,将 dp 左移 g 位后与原 dp 进行 OR 操作,表示在现有子集和基础上加入 g 后可能的子集和。
    • 最终检查 dp[target] 是否为 1,即是否存在子集和为 target。
  3. 输出结果:

    • 如果存在这样的划分,输出 Yes;否则,输出 No。
       // 计算 γ 值
       vector<int> g(N + 1, 0);   // g[0] 表示首都
       for(int i = 0; i <= N; i ++)
           if(dist_bfs[i] != -1) g[i] = dist_bfs[i] +1;
           else g[i] = 0;
       // 收集所有 γ 值
       vector<int> gv;
       for(int i = 0; i <= N; i ++) 
           if(g[i] > 0) gv.push_back(g[i]);
       // 计算总和
       ll total_sum = 0;
       for(auto &g: gv)
           total_sum += g;
       // 如果总和为奇数,则不可能平分
       if(total_sum % 2 !=0)
       {
           cout << "No\n";
           return 0;
       }
       ll target = total_sum / 2;
  	 // 使用位运算优化的动态规划实现
  	 // 由于 target 可能高达 500*500=250,000,我们使用 bitset
	 	 // C++ 的 bitset 不能动态定义大小较大,一般最大到几百万
  	 // 这里 target <= 250,000,可以使用 bitset<250001>
       const int MAX_SUM = 250001;
       if(target > MAX_SUM)
       {
           // 由于 N <=500, gamma_i <= 1000
           cout << "No\n";
           return 0;
       }
  
       bitset<250001> dp;
       dp[0] = 1;
  
       for(auto &g: gv) dp |= (dp << g);
  
       if(target <= 250000 && dp[target]) cout << "Yes\n";
       else cout << "No\n";

完整代码:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 定义城市结构,包含坐标
struct city {
    ll x;
    ll y;
};
// 计算两个城市之间的欧几里得距离的平方
ll dist(const city& a, const city& b) {
    ll dx = a.x - b.x;
    ll dy = a.y - b.y;
    return dx * dx + dy * dy;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int N;
    cin >> N;
    
    // 城市编号从 0 到 N,城市 0 是首都 (0,0)
    vector<city> cities(N + 1, City{0, 0});
    for(int i = 1; i <= N; i ++){
        cin >> cities[i].x >> cities[i].y;
    }
    
    // 预计算每个城市到首都的距离平方
    vector<ll> cap(N + 1, 0);
    for(int i = 0; i <= N; i ++)
        cap[i] = dist(cities[i], cities[0]);
    
    // 构建邻接表
    vector<vector<int>> adj(N + 1, vector<int>());
    
    for(int i = 1; i <= N; i ++)
    {
        ll min = LLONG_MAX;
        int sj = -1;
        for(int j = 0; j <= N; j ++)
        {
            if(j == i) continue;
            // 满足 dis(j,0) <= dis(i,0)
            if(cap[j] <= cap[i])
            {
                ll ij = dist(cities[i], cities[j]);
                if(ij < min)
                {
                    min = ij;
                    sj = j;
                }
                else if(ij == min && j < sj)
                    sj = j;
            }
        }
        if(sj != -1)
        {
            adj[i].push_back(sj);
            adj[sj].push_back(i);
        }
    }
    
    
    // BFS 从首都 (0) 开始,计算最短路径
    vector<int> dist_bfs(N+1, -1);  // -1 表示未访问
    queue<int> q;
    q.push(0);
    dist_bfs[0] = 0;
    
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(auto &v: adj[u])
            if(dist_bfs[v] == -1)
            {
                dist_bfs[v] = dist_bfs[u] + 1;
                q.push(v);
            }
    }
    
    // 计算 γ 值
    vector<int> g(N + 1, 0);  // g[0] 表示首都
    for(int i = 0; i <= N; i ++)
        if(dist_bfs[i] != -1) g[i] = dist_bfs[i] +1;
        else g[i] = 0;
    // 收集所有 γ 值
    vector<int> gv;
    for(int i = 0; i <= N; i ++) 
        if(g[i] > 0) gv.push_back(g[i]);
    // 计算总和
    ll total_sum = 0;
    for(auto &g: gv)
        total_sum += g;
    // 如果总和为奇数,则不可能平分
    if(total_sum % 2 !=0)
    {
        cout << "No\n";
        return 0;
    }
    
    ll target = total_sum / 2;
    // 使用位运算优化的动态规划实现
    // 由于 target 可能高达 500*500=250,000,我们使用 bitset
    // C++ 的 bitset 不能动态定义大小较大,一般最大到几百万
    // 这里 target <= 250,000,可以使用 bitset<250001>
    const int MAX_SUM = 250001;
    if(target > MAX_SUM)
    {
        // 由于 N <=500, g_i <= 1000
        cout << "No\n";
        return 0;
    }
    
    // 代码解释:这里 bitset<250001> dp 创建了一个大小为 250001 的位集(全部初始化为 0),然后将 dp[0] 设为 1,表示可以凑成0
    bitset<250001> dp;
    dp[0] = 1;
    
    // 代码解释:假设我们有一个整数数组 nums,目标是找到一些子集使总和为某一值。通过位移操作 dp << num,我们将当前状态集合 dp 中所有为1的位置向左移动 num 位,并将结果与原来的 dp 进行或运算 |=, 来更新状态。
    for(auto &g: gv) dp |= (dp << g);
    
    if(target <= 250000 && dp[target]) cout << "Yes\n";
    else cout << "No\n";
    
    return 0;
}

示例验证:

输入:

4
-1 -1
1 0
1 -2
-2 2

执行过程:

  1. 构建图后,连接关系为:
    • 城市 1 连接城市 0
    • 城市 2 连接城市 0
    • 城市 3 连接城市 2
    • 城市 4 连接城市 0
  2. 计算 γ 值:
    • γ[0] = 1
    • γ[1] = 2
    • γ[2] = 2
    • γ[3] = 3
    • γ[4] = 2
  3. γ 值总和为 1 + 2 + 2 + 3 + 2 = 10,目标子集和为 5
  4. 检查是否存在子集和为 5
    • 存在子集 {2, 3},其和为 5

输出:

Yes

本蒟蒻的第一篇题解完结OvO

想多了都是问题,做多了都是答案,愿每一位努力的同学都不负梦想,与大家共勉.

posted @ 2024-11-02 16:16  He_Yan_Bo  阅读(107)  评论(0)    收藏  举报