WHUT第四周训练整理
写在前面的话:我的能力也有限,错误是在所难免的!因此如发现错误还请指出一同学习!
索引
(这里的难度仅由本周做题情况进行分类,仅供新生参考!)
一、easy:01、02、03、04、05、07、09
二、medium:08、10、11、12、13、14
三、hard:06
四、乱入的分治:15
本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。
一、easy
1001:FatMouse’ Trade
题意:有 N 个房间,第i个房间有 J[i] 的宝物,同时需要 F[i] 的食物,可以只取走部分宝物,这种情况下只需要支付等比例的食物。给定 M 的食物,求可以获得的最大价值(宝物)。
分析:因为能够取走部分的宝物,所以我们优先去“性价比”最高的房间,即 J [ i ] F [ i ] \frac{J[i]}{F[i]} F[i]J[i] 最大的房间,一直取直到不能取为止。因此我们按照“性价比”排序,能全部取完的直接取完,不能全部取完的取走部分即可。
Notice:这题个人使用cout就会Wrong Answer,换成printf就过了…剧毒!
Code:
const int MAXN = 1000 + 10;
int n, m;
struct Node
{
double value, cost, rate;
bool operator<(Node other) const
{
return rate > other.rate; // 按照性价比排序
}
} nodes[MAXN];
int main()
{
while (cin >> m >> n)
{
if (m == -1 && n == -1)
break;
for (int i = 0; i < n; i++)
{
cin >> nodes[i].value >> nodes[i].cost;
nodes[i].rate = (double)nodes[i].value / nodes[i].cost; // 注意类型转换
}
sort(nodes, nodes + n);
double ans = 0;
for (int i = 0; i < n; i++)
{
if (m >= nodes[i].cost) // 能取完就全部取完
{
ans += nodes[i].value;
m -= nodes[i].cost;
}
else // 否则取走部分
{
ans += (nodes[i].value / nodes[i].cost) * m;
break;
}
}
printf("%.3f\n", ans);
// cout << fixed << setprecision(3) << ans << endl;
}
return 0;
}
1002:Climbing Worm
题意:位于井里 n 米深的蜗牛每分钟可以爬上 u 米,必须休息一分钟之后才可以继续爬,休息一分钟会下滑 d 米,问爬出井需要多久。
分析:爬出井前的最后一分钟不需要滑落,因此我们计算进行一次爬和一次降之后的增量 u-d,以这个速度到达 n-u 的位置再爬一步就可以出去了。通过 ⌈ n − u u − d ⌉ ∗ 2 + 1 \left \lceil \frac{n-u}{u-d} \right \rceil*2+1 ⌈u−dn−u⌉∗2+1 计算即可。
Notice:ceil(double x)函数表示对浮点数x向上取整。
Code:
int main()
{
int n, u, d;
while(cin >> n >> u >> d, n+u+d){
cout << (int)ceil((n-u)*1.0/(u-d))*2+1 << endl;
}
return 0;
}
1003:Game Prediction
题意:有 m 个人,每个人发 n 张牌,每张牌的点数都不一样,一共有 n*m 张牌,点数为 1~n*m。现在要进行 n 轮比赛,每轮比赛大家出一张牌,点数最大的玩家获胜,现在给你当前手上的 n 张牌,求至少可以获胜的局数。
分析:按最坏的情况想,只要当前别人的手里还有比你大的牌,那么自己必输,否则必赢。因此可以从大到小枚举点数,确定当前场上别人手中最大的牌,如果比自己最大的牌小那么ans++,否则自己这张牌作废。详见代码。
Code:
const int MAXN = 1000+10;
int arr[MAXN];
int vis[MAXN];
int main()
{
int n, m;
int kase = 1;
while(cin >> m >> n, n+m){
memset(vis, 0, sizeof(vis)); // 数组置为0
for(int i = 0; i < n; i++){
cin >> arr[i];
vis[arr[i]] = 1; // 标记,别人手里不会有这张牌
}
sort(arr, arr+n); // 对自己的手牌进行排序
int maxV = n*m;
int ans = 0;
for(int i = n-1; i >= 0; i--){ // 先确定自己要出的牌
while(vis[maxV]) maxV--; // 找到场上别人手里最大的牌
if(arr[i] < maxV){ // 如果没有比别人最大的牌大则作废
vis[maxV] = 1; // 别人手里不会有这张牌了
}
else{ // 否则本轮获胜
ans++;
}
}
cout << "Case " << kase++ << ": " << ans << endl;
}
return 0;
}
1004:Doing Homework again
题意:给定 n 个任务的截止日期,超过则会扣相应的分数,问怎么安排才能达到扣最少的分数。每个任务都需要做一天。
分析:扣分最多的任务应该考虑,先从这个任务的 deadline 开始往前扫描看是否有空闲的日期,有则安排,没有的话只能扣分,因为如果这天安排其他的任务都会导致扣分更多。
Code:
const int MAXN = 1000+10;
struct Node {
int ddl, value;
bool operator < (Node other) const {
return value > other.value; // 按照扣分从大到小排序
}
}nodes[MAXN];
int vis[MAXN];
int main()
{
int T, n;
cin >> T;
while(T--){
memset(vis, 0, sizeof(vis)); // 清空vis标记数组
cin >> n;
for(int i = 0; i < n; i++){
cin >> nodes[i].ddl;
}
for(int i = 0; i < n; i++){
cin >> nodes[i].value;
}
sort(nodes, nodes+n); // 按照扣分排序
int ans = 0;
for(int i = 0; i < n; i++){
for(int j = nodes[i].ddl; j >= 1; j--){ // 从ddl开始往前扫描
if(!vis[j]){ // 有空位则填入
vis[j] = 1;
break;
}
if(j == 1){ // 没有空位了就扣分
ans += nodes[i].value;
}
}
}
cout << ans << endl;
}
return 0;
}
1005:Shopaholic
题意:有购物优惠活动,当购买 >= 3 件物品时,其中最便宜的物品可以免单,可以将物品分批支付。给 n 个物品,问怎么安排可以得到最大的优惠。
分析:显然应该三件三件的拿去支付,优惠取决于三件物品中最便宜的物品,这个最便宜的物品越贵越好。因此我们先按价格排序,每次拿最贵的三件物品去结算,折扣就是三件中最便宜的那件,累加答案即可。
Code:
const int MAXN = 20000+10;
int goods[MAXN];
int main()
{
int T, n;
cin >> T;
while(T--){
cin >> n;
for(int i = 0; i < n; i++){
cin >> goods[i];
}
sort(goods, goods+n); // 按价格从小到大排序
int ans = 0;
for(int i = n-3; i >= 0; i -= 3){ // 每次取出3个最贵的物品
ans += goods[i];
}
cout << ans << endl;
}
return 0;
}
1007:最少拦截系统
题意:给出 n 个导弹的高度,问最少需要多少个高度不增的拦截系统才能全部拦截成功。
分析:对于每个拦截系统,我们希望它尽可能多的拦截导弹,即每次拦截一个导弹时高度损失尽可能小,这样才能为后面的拦截提供更多的可能性。因此我们可以对每颗导弹去找前面已经设置的拦截系统中,高度差最小的拦截系统,用这个拦截系统来拦截这颗导弹,如果没有就新增一个拦截系统。
Code:
const int MAXN = 30000+10;
int arr[MAXN];
vector<int> sys; // 因为不确定拦截系统的数量,所以用vector容器保存每个拦截系统的高度
int main()
{
int n;
while(cin >> n){
sys.clear(); // 使用vector记得清空
for(int i = 0; i < n; i++){
cin >> arr[i];
}
int ans = 0;
for(int i = 0; i < n; i++){
int minV = INF, index = -1;
for(int j = 0; j < sys.size(); j++){ // 尝试去寻找高度差最小的拦截系统
if(sys[j] >= arr[i]){
if(minV > sys[j]-arr[i]){
minV = sys[j]-arr[i];
index = j;
}
}
}
if(index == -1){ // 没有找到的话就新增一个系统
sys.push_back(arr[i]);
ans++;
}
else{ // 找到了就更新这个拦截系统的当前高度
sys[index] = arr[i];
}
}
cout << ans << endl;
}
return 0;
}
1009:Saving HDU
题意:给 n 个宝贝的单价以及总的体积,宝贝可以分割,现在口袋大小为 v ,问可以取到的最大价值是多少。
分析:与1001类似,排序后贪心地取就可以了,不再赘述。
Notice:Sample Output中的中文不要输出!
Code:
const int MAXN = 100 + 10;
struct Node
{
int cost, v;
bool operator<(Node other) const
{
return cost > other.cost; // 按照单价从大到小排序
}
} nodes[MAXN];
int main()
{
int n, v;
while (cin >> v, v)
{
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> nodes[i].cost >> nodes[i].v;
}
sort(nodes, nodes + n);
int ans = 0;
for (int i = 0; i < n; i++)
{
if (v >= nodes[i].v) // 能全部取完就取完
{
ans += nodes[i].cost * nodes[i].v;
v -= nodes[i].v;
}
else // 不能取完就取部分
{
ans += nodes[i].cost * v;
break;
}
}
cout << ans << endl;
}
return 0;
}
二、medium
1008:Flying to the Mars
题意:有 n 个飞行员,他们都有自己的等级,现在他们需要扫帚来练习飞行。规定高等级的飞行员可以当低等级飞行员的老师,他们可以共享一个扫帚,而每个飞行员最多只能有一个老师和一个徒弟,问至少需要多少扫帚来所有飞行员练习飞行。
分析:如果飞行员的等级都不相同,对他们进行排序,等级最高的当等级次高的老师,而等级次高的可以当等级第三高的老师… 以此来推,那么只需要一个扫帚就够了;如果有两个飞行员的等级相同,那么至少需要两个扫帚;如果有三个飞行员的等级相同,那么至少需要三个扫帚。
因此发现如果飞行员的等级出现了重复,那么就必须使用新的扫帚才能满足需求,因此我们只需要求出最多的相同等级的人数就可以了,使用C++的map就可以解决。
Code:
map<int, int> mp; // 表示等级到个数的映射
int main()
{
int n;
while(~scanf("%d", &n)){
mp.clear(); // 清空map
int ans = 0;
for(int i = 0; i < n; i++){
int v;
scanf("%d", &v);
mp[v]++; // 等级为v的个数+1
ans = max(ans, mp[v]); // 求最大值
}
printf("%d\n", ans);
}
return 0;
}
1010:Buildings
题意:从上到下盖楼层,每个楼层都有自己的重量 wi 以及强度 si,在盖楼中第 i 层的 PDV 被定义为 ∑ j = 0 i − 1 w j − s i \sum_{j = 0}^{i-1}w_j-s_i ∑j=0i−1wj−si,建成之后整个建筑的 PDV 为所有楼层中最大的 PDV。现在问怎么盖楼才能让最后建筑的 PDV 最小。
分析:因为负数的 PDV 将当成 0 来处理,所以不能无脑把重量最小的放上面。
考虑上下两个楼层的顺序,怎么摆放能让整体的 PDV 更少。
设第一个楼层 w1, s1,第二个楼层 w2, s2,上面楼层的总重量为 w。
先放一后放二的结果是 max(w - s1, w + w1 - s2); // 标记为 1,2
先放二后放一的结果是 max(w - s2, w + w2 - s1) 。 // 标记为 3,4
我们假设第一种方案比第二种更优,那么需要满足什么条件呢?
分类讨论:
若 1 >= 2,而 1 < 4,故满足条件;
若 1 < 2,只需要考虑 2 与 4 的大小,需要满足 w1 + s1 < w2 + s2 。
所以只要按照 wi + si 进行排序就可以保证最优性。
Code:
const int MAXN = 1e5 + 10;
int n;
struct Node
{
int w, s;
bool operator<(Node other) const
{
return w + s < other.w + other.s; // 按照w+s排序
}
} nodes[MAXN];
int main()
{
while (cin >> n)
{
for (int i = 0; i < n; i++)
{
cin >> nodes[i].w >> nodes[i].s;
}
sort(nodes, nodes + n);
int ans = 0;
int sum = 0; // 统计上面的总重量
for (int i = 0; i < n; i++)
{
ans = max(ans, sum - nodes[i].s < 0 ? 0 : sum - nodes[i].s); // 注意负数当成0处理
sum += nodes[i].w;
}
cout << ans << endl;
}
return 0;
}
1011:I can do it!
题意:有 n 个元素,每个元素都有 Ai 和 Bi 两个属性,现在要求把这 n 个元素分成两个集合 A 和 B,A 中的元素保留 Ai,B 中的元素保留 Bi 。问怎么进行分割,让**(A 中的最大值 + B中的最大值)**最小。
分析:看这个数据范围就知道不能暴力了,我们考虑怎么在确定了 A 中的最大值之后快速确定 B 中的最大值。可以先按照 Ai 的大小进行排序,预处理数组 maxV,maxV[i] 表示从 i 以后最大的 B 值,之后我们从前往后扫描,i 以及 i 之前的元素划分到集合 A 中,i 之后的元素划分到集合 B 中,那么此时集合 A 中的最大值就是 Ai,而 B 中的最大值就是 max[i],将两者加起来更新答案即可。详见代码。
Notice:集合中不一定有元素!
Code:
const int MAXN = 1e5 + 10;
const int INF = 0x3f3f3f3f;
struct Node {
int a, b;
bool operator < (Node other) const {
return a < other.a; // 按照 a 排序
}
}nodes[MAXN];
int maxV[MAXN];
int main()
{
int T, n;
cin >> T;
int kase = 1;
while(T--){
memset(maxV, 0, sizeof(maxV)); // 清空数组
cin >> n;
for(int i = 0; i < n; i++){
cin >> nodes[i].a >> nodes[i].b;
}
sort(nodes, nodes+n);
int maxValue = 0; // 保存从后往前的最大值
for(int i = n-1; i >= 0; i--){
maxValue = max(maxValue, nodes[i].b);
maxV[i] = maxValue; // 预处理 maxV 数组
}
int ans = INF; // 设置为最大值
for(int i = 0; i < n; i++){
ans = min(ans, nodes[i].a+maxV[i+1]); // 更新答案,取最小值
}
cout << "Case " << kase++ << ": " << ans << endl;
}
return 0;
}
1012:Physical Examination
题意:有很多队列需要去排队,每个队列都有自己的初始长度 Ai,而且会随着时间每秒增加 Bi(正在排的队就不影响了),问怎么安排让总排队时间最少。
分析:类似于1010的思考方式,考虑两个队列哪个先排哪个后排才能让整体的排队时间最少。
设第一个队列 A1, B1,第二个队列 A2, B2,已经流逝的时间为 t 。
那么先排一后排二的总时间
T 1 = A 1 + B 1 × t + A 2 + B 2 × ( t + A 1 + B 1 × t ) T_1 = A_1 + B_1\times{t} +A_2+B_2\times{(t+A_1+B_1\times{t})} T1=A1+B1×t+A2+B2×(t+A1+B1×t)
先排二后排一的总时间
T 2 = A 2 + B 2 × t + A 1 + B 1 × ( t + A 2 + B 2 × t ) T_2 = A_2 + B_2\times{t} +A_1+B_1\times{(t+A_2+B_2\times{t})} T2=A2+B2×t+A1+B1×(t+A2+B2×t)
假设 T 1 < T 2 T_1 < T_2 T1<T2,那么化简后得到 A 1 × B 2 < A 2 × B 1 A_1\times{B_2} < A_2\times{B_1} A1×B2<A2×B1
因此按照这个规则进行排序,统计答案就可以了。
Code:
const int MAXN = 1e5 + 10;
const int MOD = 365 * 24 * 60 * 60;
struct Node
{
int a, b;
bool operator<(Node other) const
{
return a * other.b < other.a * b; // 按照规则排序
}
} nodes[MAXN];
int main()
{
int n;
while (cin >> n, n)
{
for (int i = 0; i < n; i++)
{
cin >> nodes[i].a >> nodes[i].b;
}
sort(nodes, nodes+n);
int ans = 0;
for(int i = 0; i < n; i++){
ans += nodes[i].a%MOD+ans*nodes[i].b%MOD; // 注意取模
}
cout << ans%MOD << endl;
}
return 0;
}
1013:Moving Tables
题意:在一条长度为 200 的走廊两侧共有 400 个房间,有 N 个桌子需要进行搬动,从一个房间出来进行到达另一个房间。现在给了这些桌子的移动路线,走廊没有重叠的桌子的移动可以同时进行,问最少需要轮完成所有桌子的移动,一轮需要10分钟,在一轮中所有进行合法移动的桌子都到达目标房间。
分析:走廊只有 200 的长度,每次移动桌子就对这个区间中的每个点计数,最后取 1~200 中的最大值*10就是答案,因为这个点至少需要这么多轮才可以满足。
Notice:区间的左右端点要正确,保证左端点 < 右端点。
Code:
const int MAXN = 200 + 10;
int cnt[MAXN];
int main()
{
int T, n;
cin >> T;
while (T--)
{
memset(cnt, 0, sizeof(cnt)); // 清空数组
cin >> n;
for (int i = 0; i < n; i++)
{
int a, b;
cin >> a >> b;
a = a % 2 ? a / 2 + 1 : a / 2; // 确定左端点
b = b % 2 ? b / 2 + 1 : b / 2; // 确定右端点
if(a > b) swap(a, b); // 别忘了这一步!
for (int j = a; j <= b; j++)
cnt[j]++; // 对区间中每个点计数
}
int ans = 0;
for (int i = 1; i <= 200; i++)
{
ans = max(ans, cnt[i]); // 取最大值
}
cout << 10 * ans << endl;
}
return 0;
}
1014:Wooden Sticks
题意:有 n 根木棍需要处理,每根木棍都有自己的长度 Li 以及重量 Wi,一开始时机器可以选择一根木根并且花费 1 分钟准备,如果接下来处理的木棍的长度以及重量都不少于之前处理的木棍,那么机器不需要花费时间准备,否则还需要 1 分钟准备,问怎么安排让总准备时间最少。
分析:我们可以贪心的让当前木棍的长度以及重量尽可能小,这样可以给后面处理的木棍更多的机会,使得总时间最少。这题有两个参数,我们对一个参数进行贪心的时候必须要保证另一个参数满足条件,所以我们考虑先对木棍的长度进行排序,接下来对重量进行贪心。详见代码。
Code:
const int MAXN = 5000 + 10;
struct Stick {
int l, w;
bool operator < (Stick &other) const {
if(l != other.l) return l < other.l; // 如果长度不同直接排序
else return w < other.w; // 否则按照重量进行排序
}
}sticks[MAXN];
int vis[MAXN]; // 标记数组,用来表示木棍是否被使用过
int main()
{
int T, n;
cin >> T;
while(T--){
memset(vis, 0, sizeof(vis)); // 清空数组
cin >> n;
for(int i = 0; i < n; i++){
cin >> sticks[i].l >> sticks[i].w;
}
sort(sticks, sticks+n); // 先排序
int ans = 0;
for(int i = 0; i < n; i++){ // 从长度最小的木棍开始,长度递增
if(vis[i]) continue; // 如果使用过则跳过
int w = sticks[i].w;
for(int j = i+1; j < n; j++){ // 在后面找到满足条件的所有木棍并且标记
if(vis[j]) continue;
if(w <= sticks[j].w){ // 找到满足要求的木棍,这根不需要准备时间
vis[j] = 1;
w = sticks[j].w; // 设置新的重量,继续往后找
}
}
ans++; // 找一轮需要1分钟准备机器
}
cout << ans << endl;
}
return 0;
}
三、hard
1006:Color a Tree
题意:给一颗有 n 个结点的树,需要给它染色,从根节点开始,每次只能给直接相连的孩子染色,比如图中 1 被染色,那么下一步可以给 2,3 染色;如果图中 1,3 被染色,那么下一步可以给 2,5 染色。初始的时间 t = 0,每次染色需要花 1 的时间,同时需要付出 *C[i]t 的代价。现在需要确定染色的顺序,使得代价最小。
分析: (比较难!我也是在网上的题解中学习的!)
首先通过分析可以发现增长速率越快的点就越需要早点染色,那么根据这个想法来贪心。
每次在所有点中选择权值最大的点,将这个点和它的父亲融合成一个点,因为我们可以知道这个点很重要,一旦染色了它的父亲,那么下一步一定是要染色这个点,而不是它的其他兄弟!
因此我们可以缩成一个点,把这个点的孩子直接连到它的父亲(类似并查集的思维),即这个点的孩子成为这个点父亲的直接孩子。
而每次缩点的时候都需要统计一下答案,ans+=trees[fa].t*trees[index].v,表示父亲的当前时间乘以最大权值点的权值。
这样缩点的操作每进行一次就会减少一个结点,因此只要进行n-1次就可以全部缩成一个点,就得到答案了。
在我们确定每个结点的增长速率的时候可以用这个点的权值/这个点的当前时间来确定,这个值越大越重要,越要早处理。
代码中计算增量部分的证明这里不列出,有兴趣的同学可以去网上搜索一下。
Code:
const int MAXN = 1000 + 10;
struct Tree
{
int t, v, pre; // 当前时间,C[i],父亲
double w; // 这个节点的权值
} trees[MAXN];
int main()
{
int n, r;
while (cin >> n >> r, n + r)
{
memset(trees, 0, sizeof(trees));
int ans = 0;
for (int i = 1; i <= n; i++)
{
cin >> trees[i].v;
trees[i].w = trees[i].v; // 权值初始值就是C[i]
trees[i].t = 1;
ans += trees[i].v; // 因为所有点的C[i]最后都至少被加上一次,这里就先处理了
}
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
trees[v].pre = u; // 确定v的父亲u
}
for (int i = 1; i < n; i++)
{
double maxV = 0;
int index = -1; // 最大权值点的索引
for (int j = 1; j <= n; j++) // 遍历所有点找到最大的权值点
{
if (j == r || maxV > trees[j].w) // 跳过根节点
continue;
index = j;
maxV = trees[j].w;
}
int fa = trees[index].pre; // 最大权值点的父亲
for (int j = 1; j <= n; j++) // 将最大权值点的孩子直接到它的父亲
{
if (trees[j].pre == index)
{
trees[j].pre = fa;
}
}
ans += trees[fa].t * trees[index].v; // 更新答案
trees[fa].t += trees[index].t; // 时间累加
trees[fa].v += trees[index].v; // C[i]累加
trees[fa].w = trees[fa].v * 1.0 / trees[fa].t; // 权值重新计算
trees[index].w = 0; // 被融合的点权值置为0,不会再被选到
}
cout << ans << endl;
}
return 0;
}
四、乱入的分治
1015:Quoit Design
题意:在二维平面上找出所有点中最近的一对。
分析: 我寻思着这不是贪心吧,这是算法中经典的分治应用。
还不了解分治的同学可以去学习一下,如果是计算机的同学在后续的算法课程中会学到分治(这道题就是例题!)。
对于这道题目,最多可能会有 1e5 个点,如果暴力一一匹配的话时间复杂度为 O(n2) ,必然会超时;而这道题使用分治算法的话时间复杂度就会降低到 O(nlog2n),这样就可以通过了。
首先,假设如图有9个点,我们要求这个平面内的最近点对,那么我们就可以先按照这些点x坐标的平均值为分割线,将这些点平均地分成左右两个部分。大问题不容易求解,就先划分成小问题进行求解,我们先求左右两个小区间的解,也就是在左右连个区间中最近的点对。
对于左边这个小区间来说,对这个区间进行求解可能还是有点难度,那么我们就如图继续取平均值进行分割。
直到区间中只有两个元素的时候,这个时候我们就知道这个区间中的最近点对就是这两个点!
那么现在我们知道了一个大区间左右两个小区间的最近点对之后,我怎么利用它们来得到当前大区间的最近点对呢?
假设如下图我们已经求出当前区间左右区间的最近点对,左边长度为 d1, 右边长度为 d2 。
那么当前区间的最近点对就是两者取较小值 min(d1, d2) 吗?不一定,因为可能存在最近点对跨越了分割线的情况!如下图。
我们还需要考虑跨越分割线的点对,那么我们处理完左右区间之后,进行合并时两两枚举左右区间中的点对呢?这个效率太低了,时间复杂度为 O(n2),如果这么做了那么我们进行分治还有什么意义呢,还不如直接在原来的大图上面两两枚举求最小值。
我们可以发现其实在两个区间中的点不都需要进行匹配,现在考虑优化!
设 d = min(d1, d2),就是两个区间最近点对的最小值。
那么如果存在跨越了分割线的点对是更优解,那么左边的点横坐标 x 1 x_1 x1 一定满足 m i d − x 1 < d mid-x_1<d mid−x1<d,在我们确定了一个左边可能形成更优解的点时,需要到右边寻找另一个点,那么另一个点的横坐标 x 2 x_2 x2 也一定满足 x 2 − m i d < d x_2-mid<d x2−mid<d 并且纵坐标 y 2 y_2 y2 要满足 ∣ y 2 − y 1 ∣ < d |y2-y1| < d ∣y2−y1∣<d,相当于右边这个点一定是落在一个 d × 2 d d\times{2d} d×2d 的矩阵中。
又因为 d × 2 d d\times{2d} d×2d 的矩阵可以被分成 6 个等大小的 d 2 × 2 × d 3 \frac{d}{2}\times\frac{2\times{d}}{3} 2d×32×d 的小矩阵,小矩阵的对角线长度为 5 × d 6 < d \frac{5\times{d}}{6} < d 65×d<d,因此在这个矩阵中的点数不会超过 6,如果超过 6 的话那么就会与 d 的值出现矛盾。
因此我们可以枚举左区间中满足 m i d − x < d mid-x < d mid−x<d 的点,然后在右边 d × 2 d d\times{2d} d×2d 的矩阵中找到另一个点,看看是否比 d 来的小,是则更新答案,否则继续枚举。
这样一来,分治与答案合并的部分都解决了,那么这道题就可以解了,详见代码。
Code:
const int MAXN = 1e5 + 10;
int n;
struct Point
{
double x, y; // 保存点的坐标
} points[MAXN], tmp[MAXN];
bool cmpX(Point a, Point b) // 自定义排序规则,按照 x 进行排序
{
return a.x < b.x;
}
bool cmpY(Point a, Point b) // 自定义排序规则,按照 y 进行排序
{
return a.y < b.y;
}
double dis(int i, int j) // 求两个 Point 之间的距离
{
double x1 = tmp[i].x, y1 = tmp[i].y, x2 = tmp[j].x, y2 = tmp[j].y;
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
double solve(int st, int ed) // 分治算法
{
if (st == ed) // 当区间内只有一个点的时候不存在点对,返回无穷大,这里也可以换成只有两个点的时候进行判断,那样就和上面分析的对应上了
return 1e9;
int mid = (st + ed) / 2; // 取平均值
double d1 = solve(st, mid), d2 = solve(mid + 1, ed); // 递归调用,先处理左右两个区间
double d = min(d1, d2); // 处理完左右区间得到 d
int index = 0; // 保存分割线左右长度 d 之内的所有点的数量
for (int i = mid; i >= st; i--) // 先得到左边点的数量
{
if (points[mid].x - points[i].x >= d)
break;
tmp[index++] = points[i];
}
for (int i = mid + 1; i <= ed; i++) // 再得到右边点的数量
{
if (points[i].x - points[mid].x >= d)
break;
tmp[index++] = points[i];
}
sort(tmp, tmp + index, cmpY); // 对得到的点按照 y 进行排序
for (int i = 0; i < index; i++) // 确定其中一个点
{
for (int j = i + 1; j < index && tmp[j].y-tmp[i].y < d; j++) // 找到另一个落入 d*2d矩阵中的点
{
d = min(d, dis(i, j)); // 更新答案
}
}
return d;
}
int main()
{
while (~scanf("%d", &n), n)
{
for (int i = 0; i < n; i++)
{
scanf("%lf%lf", &points[i].x, &points[i].y);
}
sort(points, points + n, cmpX); // 先按照 x 进行排序,这样好进行分割
double d = solve(0, n - 1); // 分治得到答案
printf("%.2f\n", d / 2);
}
return 0;
}
【END】感谢观看!