WHUT第四周训练整理

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 udnu2+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=0i1wjsi,建成之后整个建筑的 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,只需要考虑 24 的大小,需要满足 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 个元素,每个元素都有 AiBi 两个属性,现在要求把这 n 个元素分成两个集合 ABA 中的元素保留 AiB 中的元素保留 Bi 。问怎么进行分割,让**(A 中的最大值 + B中的最大值)**最小。

分析:看这个数据范围就知道不能暴力了,我们考虑怎么在确定了 A 中的最大值之后快速确定 B 中的最大值。可以先按照 Ai 的大小进行排序,预处理数组 maxVmaxV[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的思考方式,考虑两个队列哪个先排哪个后排才能让整体的排队时间最少。

设第一个队列 A1B1,第二个队列 A2B2,已经流逝的时间为 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 midx1<d,在我们确定了一个左边可能形成更优解的点时,需要到右边寻找另一个点,那么另一个点的横坐标 x 2 x_2 x2 也一定满足 x 2 − m i d < d x_2-mid<d x2mid<d 并且纵坐标 y 2 y_2 y2 要满足 ∣ y 2 − y 1 ∣ < d |y2-y1| < d y2y1<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 midx<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】感谢观看!

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值