小韦老师@NOIP 普及组-2008-排座椅
题目:
描述
上课的时候总会有一些同学和前后左右的人交头接耳,这是令小学班主任十分头疼的一件事情。不过,班主任小雪发现了一些有趣的现象,当同学们的座次确定下来之后,只有有限的 D 对同学上课时会交头接耳。
同学们在教室中坐成了 M 行 N 列,坐在第 i 行第 j 列的同学的位置是 (i, j),为了方便同学们进出,在教室中设置了 K 条横向的通道,L 条纵向的通道。于是,聪明的小雪想到了一个办法,或许可以减少上课时学生交头接耳的问题:她打算重新摆放桌椅,改变同学们桌椅间通道的位置,因为如果一条通道隔开了两个会交头接耳的同学,那么他们就不会交头接耳了。
请你帮忙给小雪编写一个程序,给出最好的通道划分方案。在该方案下,上课时交头接耳的学生的对数最少。
输入
输入的第一行,有 5 个用空格隔开的整数,分别是 M,N,K,L,D(2 <= N,M <= 1000,0 <= K < M,0 <= L < N,D <= 2000)。
接下来的 D 行,每行有 4 个用空格隔开的整数。第 i 行的 4 个整数 Xi,Yi,Pi,Qi,表示坐在位置 (Xi,Yi) 与 (Pi,Qi) 的两个同学会交头接耳(输入保证他们前后相邻或者左右相邻)。
输入数据保证最优方案的唯一性。
输出
输出共两行。
第一行包含 K 个整数,a1,a2……aK,表示第 a1 行和 a1+1 行之间、第 a2 行和 a2+1 行之间、…、第 aK 行和第 aK+1 行之间要开辟通道,其中 ai < ai+1,每两个整数之间用空格隔开(行尾没有空格)。
第二行包含 L 个整数,b1,b2……bL,表示第 b1 列和 b1+1 列之间、第 b2 列和 b2+1 列之间、…、第 bL 列和第 bL+1 列之间要开辟通道,其中 bi < bi+1,每两个整数之间用空格隔开(列尾没有空格)。
输入样例1
4 5 1 2 3
4 2 4 3
2 3 3 3
2 5 2 4
输出样例1
2
2 4
提示
上图中用符号 *、※、+ 标出了 3 对会交头接耳的学生的位置,图中 3 条粗线的位置表示通道,图示的通道划分方案是唯一的最佳方案。
题解:
思路:
整体思路:
这道题可以用贪心求解,这里贪的是分隔的人数,尽可能多是将可能会交头接耳的两个同学分隔开。
1.对于划分 k 条横向通道:
如果把每两行之间划分通道能分隔的人数求出来,将这些人数从多到少排序,取前 k 个通道即可。
例如:
r 数组表示划分通道能分隔的人数,r[1] 表示把第 1 行和第 2 行划分通道能分隔的人数,r[2] 表示把第 2 行和第 3行划分通道能分隔的人数, ……,r[m-1] 表示把第 m-1 行和第m 行划分通道能分隔的人数。将 r 数组降序排列,取前 k 个,则就是划分通道的位置了。
2.对于划分 l 条纵向通道:
如果把每两列之间划分通道能分隔的人数求出来,将这些人数从多到少排序,取前 l 个通道即可。
例如:
c 数组表示划分通道能分隔的人数,c[1] 表示把第 1 列和第 2 列划分通道能分隔的人数,c[2] 表示把第 2 列和第 3列划分通道能分隔的人数, ……,c[n-1] 表示把第 n-1 列和第n 列划分通道能分隔的人数。将 c 数组降序排列,取前 l 个,则就是划分通道的位置了。
但是上面的思路有什么问题呢?
我们的两个数组都是一样的,下标表示划分通道的位置,对应的数组的值表示分隔的人数,
题目要求我们输出的是位置,而数组排序之后,数组的下标和对应的值不能再像原来那样对应了。
例如:原来 r[1] 代表的是把第 1 行和第 2 行划分通道能分隔的人数;而排完序后,r[1] 表示的是通过划分通道能分隔出的最多的人数,至于在什么位置划分,已经不知道了。所以我们需要将划分通道的位置和人数做“绑定”,故用结构体数组来存储。
具体步骤:
1.定义结构体以及结构体数组:
const int N = 1e3 + 10;
struct node {
// 位置,表示第 pos 行和第 pos+1 行划分通道,或者表示第 pos 列和 第 pos+1 列划分通道
int pos;
int num; // 划分通道能隔开的人数
} r[N], c[N]; // r 表示行横向划分通道,c 表示纵向划分通道
2.定义 m, n, k, l, d,并且输入:
int m, n; // 教室是 m 行 n 列的
int k, l; // 需设置 K 条横向的通道,L 条纵向的通道
int d; // 现在有 d 对同学会交头接耳
cin >> m >> n >> k >> l >> d;
3.定义变量,用来接收输入的会交头接耳的两位同学的坐标:
int x1, y1; // 表示会交头接耳的第 1 位同学的位置
int x2, y2; // 表示会交头接耳的第 2 位同学的位置
4.将 d 对会交头接耳的同学的坐标输入,并且存储:
for (int i = 0; i < d; i++) {
cin >> x1 >> y1 >> x2 >> y2;
int x = min(x1, x2); // 将两位同学的最大横坐标记下
int y = min(y1, y2); // 将两位同学的最大纵坐标记下
if (x1 == x2) { // 若两位同学的横坐标相同,则需要加纵向的通道进行分隔
// 将 y 作为结构体数组 c 的下标是为了能将第 y 条通道分隔的人加 1
c[y].pos = y; // 将位置记下
c[y].num++; // 对应位置分隔的人数加 1
} else { // 若两位同学的纵坐标相同,则需要加横向的通道进行分隔
// 将 x 作为结构体数组 r 的下标是为了能将第 x 条通道分隔的人加 1
r[x].pos = x; // 将位置记下
r[x].num++; // 对应位置分隔的人数加 1
}
}
5.排序:
// 根据 r 的定义,下标是表示位置的,所以下标范围是 1~m-1
// 根据 c 的定义,下标是表示位置的,所以下标范围是 1~n-1
sort(r + 1, r + m, cmp1); // 根据分隔人数从多到少排列
sort(c + 1, c + n, cmp1); // 根据分隔人数从多到少排列
sort(r + 1, r + k + 1, cmp2); // 将前 k 个按照位置从小到大排列
sort(c + 1, c + l + 1, cmp2); // 将前 l 个按照位置从小到大排列
6.输出前 k 个横向通道的位置
for (int i = 1; i <= k; i++) {
cout << r[i].pos;
if (i < k) cout << " "; // 记得行末无空格
}
7.输出前 l 个横向通道的位置
for (int i = 1; i <= l; i++) {
cout << c[i].pos;
if (i < l) cout << " "; // 记得行末无空格
}
8.cmp1 函数的实现:
bool cmp1(node a, node b) { // 按照分隔人数从多到少排列
return a.num > b.num; // 可以理解成分隔人数多的放到前面
}
9.cmp2 函数的实现:
bool cmp2(node a, node b) { // 按照位置从小到大排列
return a.pos < b.pos; // 可以理解成位置小的放在前面
}
思考:
1°本题哪里体现了贪心的思想?请说明理由
2°本题的这种贪心求法为什么能求得最优解?请说明理由
3°输入数据时,若结构体数组的下标不是 y 和 x,而是改成以下,是否正确?请说明理由
int cnt1 = 0; // 作为结构体数组 c 下标
int cnt2 = 0; // 作为结构体数组 r 下标
for (int i = 0; i < d; i++) { // 将 d 对会交头接耳的同学的坐标输入,并且存储
cin >> x1 >> y1 >> x2 >> y2;
int x = min(x1, x2); // 将两位同学的最大横坐标记下
int y = min(y1, y2); // 将两位同学的最大纵坐标记下
if (x1 == x2) { // 若两位同学的横坐标相同,则需要加纵向的通道进行分隔
c[cnt1].pos = y;
c[cnt1].num++;
cnt1++;
} else { // 若两位同学的纵坐标相同,则需要加横向的通道进行分隔
r[cnt2].pos = x;
r[cnt2].num++;
cnt2++;
}
}
4°通过这个题目,你有哪些收获?这些收获你能学以致用麽?
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
struct node {
// 位置,表示第 pos 行和第 pos+1 行划分通道,或者表示第 pos 列和 第 pos+1 列划分通道
int pos;
int num; // 划分通道能隔开的人数
} r[N], c[N]; // r 表示行横向划分通道,c 表示纵向划分通道
bool cmp1(node a, node b) { // 按照分隔人数从多到少排列
return a.num > b.num; // 可以理解成分隔人数多的放到前面
}
bool cmp2(node a, node b) { // 按照位置从小到大排列
return a.pos < b.pos; // 可以理解成位置小的放在前面
}
int main() {
int m, n; // 教室是 m 行 n 列的
int k, l; // 需设置 K 条横向的通道,L 条纵向的通道
int d; // 现在有 d 对同学会交头接耳
cin >> m >> n >> k >> l >> d;
int x1, y1; // 表示会交头接耳的第 1 位同学的位置
int x2, y2; // 表示会交头接耳的第 2 位同学的位置
for (int i = 0; i < d; i++) { // 将 d 对会交头接耳的同学的坐标输入,并且存储
cin >> x1 >> y1 >> x2 >> y2;
int x = min(x1, x2); // 将两位同学的最大横坐标记下
int y = min(y1, y2); // 将两位同学的最大纵坐标记下
if (x1 == x2) { // 若两位同学的横坐标相同,则需要加纵向的通道进行分隔
// 将 y 作为结构体数组 c 的下标是为了能将第 y 条通道分隔的人加 1
c[y].pos = y; // 将位置记下
c[y].num++; // 对应位置分隔的人数加 1
} else { // 若两位同学的纵坐标相同,则需要加横向的通道进行分隔
// 将 x 作为结构体数组 r 的下标是为了能将第 x 条通道分隔的人加 1
r[x].pos = x; // 将位置记下
r[x].num++; // 对应位置分隔的人数加 1
}
}
// 根据 r 的定义,下标是表示位置的,所以下标范围是 1~m-1
// 根据 c 的定义,下标是表示位置的,所以下标范围是 1~n-1
sort(r + 1, r + m, cmp1); // 根据分隔人数从多到少排列
sort(c + 1, c + n, cmp1); // 根据分隔人数从多到少排列
sort(r + 1, r + k + 1, cmp2); // 将前 k 个按照位置从小到大排列
sort(c + 1, c + l + 1, cmp2); // 将前 l 个按照位置从小到大排列
for (int i = 1; i <= k; i++) { // 输出前 k 个横向通道的位置
cout << r[i].pos;
if (i < k) cout << " "; // 记得行末无空格
}
cout << endl;
for (int i = 1; i <= l; i++) { // 输出前 l 个横向通道的位置
cout << c[i].pos;
if (i < l) cout << " "; // 记得行末无空格
}
return 0;
}