找到大多数不可订购的物品
Posted
技术标签:
【中文标题】找到大多数不可订购的物品【英文标题】:Finding a majority of unorderable items 【发布时间】:2015-06-15 09:27:32 【问题描述】:我在寻找这项任务的解决方案时遇到了这个问题。
您有 N 个学生和 N 个课程。学生只能参加一门课程 一个课程可以有很多学生参加。两名学生是 同学们,如果他们参加相同的课程。如何确定是否存在 N个学生中有N/2个同学有这个吗?
条件:可以带两个学生,问他们是不是同学 你能得到的唯一答案是“是”或“否”。你需要这样做 在 O(N*log(N)) 中。
我只需要知道如何制作它,伪代码就可以了。我想它会像合并排序一样划分学生列表,这给了我复杂性的对数部分。任何想法都会很棒。
【问题讨论】:
您能否向我们展示一些您迄今为止尝试过的算法,以便我们看到您已尝试解决此问题? 明确一点,我们需要找到>=N/2
还是>N/2
?我隐约记得上次看到这个问题时>=
要容易得多。
>=N/2...如果最后有N/2个同学,我们需要填写...
【参考方案1】:
首先,将每个学生配对(1&2、3&4、5&6...等),然后检查哪些学生在同一个班级。这对中的第一个学生得到“提升”。如果有一个“古怪”的学生,他们在自己的班级,所以他们也会得到提升。如果单个班级包含 >=50% 的学生,则 >=50% 的晋升学生也在该班级。如果没有学生被提升,那么如果一个班级包含 >=50% 的学生,那么第一个或第二个学生必须在班级中,所以只需提升他们两个。这导致 >=50% 的促销活动在大班中。这总是需要 ⌊N/2⌋ 比较。
现在当我们检查提拔的学生时,如果一个班级包含 >=50% 的学生,那么 >=50% 的提拔学生在这个班级。因此,我们可以简单地递归,直到达到一个停止条件:晋升的学生少于三个。在每个步骤中,我们都会提升
如果晋升的学生少于 3 名,那么我们知道如果 >=50% 的原始学生在班级中,那么这些剩余学生中至少有一名在该班级。因此,我们可以简单地将每个原始学生与这些被提升的学生进行比较,这将显示 (A) 班级 >=50% 的学生,或 (B) 没有班级有 >=50% 的学生。这最多需要 (N-1) 次比较,但只发生一次。请注意,有可能所有原始学生与剩下的两个学生中的一个平均匹配,这检测到两个班级都有 = 50% 的学生。
所以复杂度是N/2 *~ log(N,2) + N-1
。然而,*~
表示我们不会在每次 log(N,2) 迭代中迭代所有 N/2 个学生,只会减少 N/2、N/4、N/8... 的分数,这总和为 N。所以总复杂度为N/2 + N/2 + N-1 = 2N-1
,当我们删除常量时,我们得到O(N)
。 (我觉得我可能在这一段中犯了数学错误。如果有人发现它,请告诉我)
在此处查看实际操作:http://coliru.stacked-crooked.com/a/144075406b7566c2(由于我在实施过程中进行了简化,比较计数可能略超过估计值)
这里的关键是,如果 > 50% 的学生在一个班级,那么 >=50% 的任意对都在那个班级,假设古怪的学生与自己匹配。一个技巧是,如果恰好 50% 匹配,则它们可能会按照原始顺序完美交替,因此没有人得到提升。幸运的是,唯一的情况是交替,所以通过提升第一和第二个学生,即使在那种边缘情况下,> = 50% 的提升是在大班上。
要证明 >=50% 的晋升是在大班上是很复杂的,我什至不确定我能清楚地说明为什么会这样。令人困惑的是,它也不适用于任何其他分数。如果目标是 >=30% 的比较,则完全有可能没有被提升的学生在目标班级中。所以 >=50% 是一个神奇的数字,它根本不是任意的。
【讨论】:
这是正确的想法,但解释可以改进。它是 Manber, U. (1989) 中“寻找多数”算法的变体。算法简介 - 一种创造性的方法。他将算法解释为归纳法,将问题简化为更小的问题。 您实际上可以使用类似方法找到有 1/4 或 1/3(或者我怀疑任何简单部分)学生的班级。【参考方案2】:如果可以知道每门课程的学生人数,那么知道是否有一门课程的学生人数 >= N/2 就足够了。在这种情况下,最坏情况下的复杂度为O(N)
。
如果无法知道每门课程的学生人数,那么您可以使用更改后的快速排序。在每个周期中,您随机选择一名学生并将其他学生分成同学和非同学。如果同学人数 >= N/2,则您停止,因为您有答案,否则您分析非同学分区。如果该分区中的学生人数 = N/2,否则您从非同学分区中选择另一个学生并仅使用非同学重复所有内容-classmates 元素。
我们从快速排序算法中得到的只是我们划分学生的方式。上述算法与排序无关。在伪代码中它看起来像这样(为了清楚起见,数组索引从 1 开始):
Student[] students = all_students;
int startIndex = 1;
int endIndex = N; // number of students
int i;
while(startIndex <= N/2)
endIndex = N; // this index must point to the last position in each cycle
students.swap(startIndex, start index + random_int % (endIndex-startIndex));
for(i = startIndex + 1; i < endIndex;)
if(students[startIndex].isClassmatesWith(students[i]))
i++;
else
students.swap(i,endIndex);
endIndex--;
if(i-startIndex >= N/2)
return true;
startIndex = i;
return false;
算法开始前的分区情况就这么简单:
| all_students_that_must_be_analyzed |
在第一次运行期间,学生集将按以下方式划分:
| classmates | to_be_analyzed | not_classmates |
在之后的每次运行中,学生集将按如下方式划分:
| to_ignore | classmates | to_be_analyzed | not_classmates |
在每次运行结束时,学生集将按以下方式进行划分:
| to_ignore | classmates | not_classmates |
此时我们需要检查 classmates 分区是否有超过 N/2 个元素。如果有,那么我们有一个肯定的结果,如果没有,我们需要检查 not_classmates 分区是否有 >= N/2 个元素。如果有,那么我们需要进行另一次运行,否则我们的结果是否定的。
关于复杂性
更深入地思考上述算法的复杂性,影响它的主要因素有两个,分别是:
-
每门课程的学生人数(无需知道该人数即可让算法发挥作用)。
算法每次迭代中找到的平均同学数。
算法的一个重要部分是要分析的学生的随机选择。
最坏的情况是每门课程有 1 名学生。在这种情况下(出于显而易见的原因,我会说)复杂性将是O(N^2)
。如果课程的学生人数不同,则不会发生这种情况。
最坏情况的一个例子是,假设我们有 10 名学生,10 门课程,每门课程有 1 名学生。第一次检查 10 名学生,第二次检查 9 名学生,第三次检查 8 名学生,以此类推。这带来了O(N^2)
的复杂性。
最佳情况是当您选择的第一个学生在一个学生人数 >= N/2 的课程中。在这种情况下,复杂度将是 O(N)
,因为它会在第一次运行时停止。
最好的情况是,我们有 10 名学生,其中 5 人(或更多)是同学,在第一次运行中,我们从这 5 名学生中挑选一个。在这种情况下,我们将只检查1次同学,找到5个同学,并返回true
。
平均案例场景是最有趣的部分(并且更接近真实场景)。在这种情况下,需要进行一些概率计算。
首先,特定课程的学生被选中的机会是[number_of_students_in_the_course] / N
。这意味着,在第一次运行中,更有可能选择一个有很多同学的学生。
话虽如此,让我们考虑这样一种情况,即每次迭代中找到的同学的平均数量小于 N/2(快速排序的平均情况下每个分区的长度也是如此)。假设在每次迭代中找到的同学的平均数量是剩余 M 个学生(不是先前选择的学生的同学)的 10%(为便于计算而采用的数量)。在这种情况下,每次迭代我们都会有这些 M 值:
M1 = N - 0.1*N = 0.9*N
M2 = M1 - 0.1*M1 = 0.9*M1 = 0.9*0.9*N = 0.81*N
M3 = M2 - 0.1*M2 = 0.9*M2 = 0.9*0.81*N = 0.729*N
我会将其四舍五入为 0.73*N
以便于计算
M4 = 0.9*M3 = 0.9*0.73*N = 0.657*N ~= 0.66*N
M5 = 0.9*M4 = 0.9*0.66*N = 0.594*N ~= 0.6*N
M6 = 0.9*M5 = 0.9*0.6*N = 0.54*N
M7 = 0.9*M6 = 0.9*0.54*N = 0.486*N ~= 0.49*N
算法停止,因为我们还有 49% 的剩余学生,而且我们的同学不能超过 N/2。
显然,在平均同学比例较小的情况下,迭代次数会更多,但是结合第一个事实(学生多的课程中的学生在早期迭代中被选中的概率更高),复杂度将趋向于O(N)
,迭代次数(在伪代码的外循环中)将(或多或少)恒定且不依赖于 N。
为了更好地解释这种情况,让我们使用更大(但更现实)的数字和超过 1 个分布。假设我们有 100 名学生(为了计算简单而取的数字),这些学生以下列(假设的)方式之一分布在课程中(这些数字只是为了解释目的而排序,它们不是必需的算法工作):
-
50、30、10、5、1、1、1、1、1
35、27、25、10、5、1、1、1
11、9、9、8、7、7、5、5、5、5、5、5、5、5、5、3、1
给出的数字也是(在这种特殊情况下)课程中的学生(不是特定学生,只是该课程的学生)在第一次运行中被选中的概率。第一种情况是我们有一半学生参加的课程。第二种情况是我们没有半数学生的课程,但有很多学生的多于一门课程。第三种情况是我们的课程分布相似。
在第一种情况下,第一门课程的学生有 50% 的概率被选中,第二门课程的学生有 30% 的概率被选中,第三门课程的学生有 10% 的概率课程被选中,第 4 门课程的学生被选中的概率为 5%,第 5 门课程的学生被选中的概率为 1%,依此类推,第 6、第 7、第 8 和第 9 门课程。第一个案例的学生提前被选中的概率更高,如果该课程的学生在第一次运行中没有被选中,那么它在第二次运行中被选中的概率只会增加。例如,假设在第一次运行中选择了第二门课程的学生。 30% 的学生将被“移除”(如“不再考虑”)并且不会在第二轮分析中进行分析。在第二轮中,我们将剩下 70 名学生。第二轮从第一门课程中挑选学生的概率是 5/7,超过 70%。让我们假设 - 运气不好 - 在第二轮比赛中,一名来自第三门课程的学生被选中。在第三轮中,我们将剩下 60 名学生,第一门课程的学生在第三轮中被选中的概率为 5/6(超过 80%)。我想说我们可以认为我们的厄运在第三轮结束了,第一门课程的学生被选中,该方法返回true
:)
对于第 2 和第 3 种情况,我将遵循每次运行的概率,只是为了计算简单。
在第 2 种情况下,我们将在第 1 次运行中选择来自第 1 门课程的学生。由于同学的数量不是 false。
同样的情况发生在第 3 种情况下(我们在每次运行中平均“移除”剩余学生的 10%),但步骤更多。
最终考虑
无论如何,最坏情况下算法的复杂度是O(N^2)
。 平均案例场景很大程度上基于概率,并且倾向于从参加者众多的课程中提早挑选学生。这种行为往往会将复杂性降低到O(N)
,这是我们在最佳情况中也有的复杂性。
算法测试
为了测试算法的理论复杂度,我用 C# 编写了以下代码:
public class Course
public int ID get; set;
public Course() : this(0)
public Course(int id)
ID = id;
public override bool Equals(object obj)
return (obj is Course) && this.Equals((Course)obj);
public bool Equals(Course other)
return ID == other.ID;
public class Student
public int ID get; set;
public Course Class get; set;
public Student(int id, Course course)
ID = id;
Class = course;
public Student(int id) : this(id, null)
public Student() : this(0)
public bool IsClassmatesWith(Student other)
return Class == other.Class;
public override bool Equals(object obj)
return (obj is Student) && this.Equals((Student)obj);
public bool Equals(Student other)
return ID == other.ID && Class == other.Class;
class Program
static int[] Sizes get; set;
static List<Student> Students get; set;
static List<Course> Courses get; set;
static void Initialize()
Sizes = new int[] 2, 10, 100, 1000, 10000, 100000, 1000000 ;
Students = new List<Student>();
Courses = new List<Course>();
static void PopulateCoursesList(int size)
for (int i = 1; i <= size; i++)
Courses.Add(new Course(i));
static void PopulateStudentsList(int size)
Random ran = new Random();
for (int i = 1; i <= size; i++)
Students.Add(new Student(i, Courses[ran.Next(Courses.Count)]));
static void Swap<T>(List<T> list, int i, int j)
if (i < list.Count && j < list.Count)
T temp = list[i];
list[i] = list[j];
list[j] = temp;
static bool AreHalfOfStudentsClassmates()
int startIndex = 0;
int endIndex;
int i;
int numberOfStudentsToConsider = (Students.Count + 1) / 2;
Random ran = new Random();
while (startIndex <= numberOfStudentsToConsider)
endIndex = Students.Count - 1;
Swap(Students, startIndex, startIndex + ran.Next(endIndex + 1 - startIndex));
for (i = startIndex + 1; i <= endIndex; )
if (Students[startIndex].IsClassmatesWith(Students[i]))
i++;
else
Swap(Students, i, endIndex);
endIndex--;
if (i - startIndex + 1 >= numberOfStudentsToConsider)
return true;
startIndex = i;
return false;
static void Main(string[] args)
Initialize();
int studentsSize, coursesSize;
Stopwatch stopwatch = new Stopwatch();
TimeSpan duration;
bool result;
for (int i = 0; i < Sizes.Length; i++)
for (int j = 0; j < Sizes.Length; j++)
Courses.Clear();
Students.Clear();
studentsSize = Sizes[j];
coursesSize = Sizes[i];
PopulateCoursesList(coursesSize);
PopulateStudentsList(studentsSize);
Console.WriteLine("Test for 0 students and 1 courses.", studentsSize, coursesSize);
stopwatch.Start();
result = AreHalfOfStudentsClassmates();
stopwatch.Stop();
duration = stopwatch.Elapsed;
var studentsGrouping = Students.GroupBy(s => s.Class);
var classWithMoreThanHalfOfTheStudents = studentsGrouping.FirstOrDefault(g => g.Count() >= (studentsSize + 1) / 2);
Console.WriteLine(result ? "At least half of the students are classmates." : "Less than half of the students are classmates");
if ((result && classWithMoreThanHalfOfTheStudents == null)
|| (!result && classWithMoreThanHalfOfTheStudents != null))
Console.WriteLine("There is something wrong with the result");
Console.WriteLine("Test duration: 0", duration);
Console.WriteLine();
Console.ReadKey();
执行时间符合平均案例场景的预期。随意使用代码,您只需复制并粘贴它,它应该可以工作。
【讨论】:
快速排序属于比较排序,需要严格的弱排序。 OP 没有严格的弱排序。有趣的是,您的算法确实有效,但由于每个分区平均不是学生的一半,因此它是一个 O(n^2) 算法。 @MooingDuck 你是对的,最坏情况下的复杂度是 O(N^2)。我添加了一些关于复杂性的更多细节以及对算法本身的更多说明。 不能比较,所以不能分区。 @NeilG,也许我无法彻底解释上述算法的工作原理,但学生之间没有相互比较。如果您查看代码,两个学生之间没有比较(即:st1 同学和不是同学,然后我们在 not classmates 分区上重复所有内容,但对于在 not classmates 分区本身上选择的不同学生。我不通过比较来划分。 啊,我明白了,但是这可能会很慢,因为可能没有一个学生是同学,在这种情况下你会分区n次。【参考方案3】:我会发表一些我的想法.. 首先,我认为我们需要做一些类似合并排序的事情,以使那个对数部分......我想,在最低级别,我们只有 2 个学生要比较,我们只需询问并得到答案。但这并不能解决任何问题。在这种情况下,我们将只有 N/2 对学生和知识,他们要么是同学,要么从来都不是。这没有帮助..
下一个想法要好一些。我没有将该组划分为最低水平,但是当我有 4 个学生组时我停止了。所以我有 N/4 个小集合,我将每个人相互比较。如果我发现其中至少有两个是同学,那就太好了。如果不是,而且他们都来自不同的班级,我完全忘记了那个 4 人小组。当我将这个应用到每个小组时,我开始通过比较那些已经被标记为同学的人将他们加入 8 人小组。 (由于传递性)。再说一次……如果至少有 4 个同学,8 人一组,我很高兴,如果没有,我就忘记了那个组。这应该重复,直到我有两组学生并对两组学生进行比较以获得最终答案。但问题是,一半可能有 n/2-1 个同学,而另一半只有一个学生与他们匹配.. 这个算法不适用于这个想法。
【讨论】:
我终于想出了我认为的答案,而你非常接近了它。以上是关于找到大多数不可订购的物品的主要内容,如果未能解决你的问题,请参考以下文章