我有未排序的整数对,它们代表一些时间间隔(第一个数字总是小于第二个数字)。问题是为每个时间间隔分配一个所谓的通道号(0..x)的整数,这样不碰撞的间隔将共享同一个通道。应使用尽可能少的频道。
例如,这些间隔将使用2个通道:
50 100 // 1
10 70 // 0
80 200 // 0
我使用计数排序实现它,按第一列对输入进行排序,然后使用线性搜索来查找彼此关联的链对。我首先将输入* const *数组复制到新数组,最后将值分配给输入数组中的正确位置。
是的,这是我从大学获得的一项任务,已经实施了,但任何人都可以告诉我如何让代码更快? 使用哪种算法,以便对的排序,链接将尽可能快?输入数组的长度最多为1000万个元素。
以下是代码:
#include <cstdlib>
#include <cstdio>
#include <iostream>
using namespace std;
struct TPhone
{
unsigned int m_TimeFrom;
unsigned int m_TimeTo;
unsigned int m_Channel;
};
class TElement
{
public:
TPhone m_data;
int index;
TElement(TPhone * const data, int index_)
{
m_data.m_TimeFrom=data->m_TimeFrom;
m_data.m_TimeTo=data->m_TimeTo;
m_data.m_Channel=-1;
index=index_;
}
TElement()
{
}
};
int FindNext(TElement** sorted_general_array, int general_array_size, int index_from)
{
for (int i=index_from+1; i<general_array_size; i++ )
{
if (sorted_general_array[i]->m_data.m_TimeFrom > sorted_general_array[index_from]->m_data.m_TimeTo)
{
if (sorted_general_array[i]->m_data.m_Channel==(unsigned int)-1)
{
return i;
}
}
}
return -1;
}
int AssignChannels(TElement **sorted_general_array, int general_array_size)
{
int current_channel=-1;
for (int i=0; i<general_array_size; i++)
{
if (sorted_general_array[i]->m_data.m_Channel==(unsigned int)-1)
{
current_channel++;
sorted_general_array[i]->m_data.m_Channel=current_channel;
//cout << sorted_general_array[i]->m_data.m_TimeFrom << " " << sorted_general_array[i]->m_data.m_TimeTo << " " << sorted_general_array[i]->m_data.m_Channel << endl;
int next_greater=i;
while (1)
{
next_greater=FindNext(sorted_general_array,general_array_size,next_greater);
if (next_greater!=-1)
{
sorted_general_array[next_greater]->m_data.m_Channel=current_channel;
//cout << sorted_general_array[next_greater]->m_data.m_TimeFrom << " " << sorted_general_array[next_greater]->m_data.m_TimeTo << " " << sorted_general_array[next_greater]->m_data.m_Channel << endl;
}
else
{
break;
}
}
}
}
return current_channel;
}
int AllocChannels ( TPhone * const * req, int reqNr )
{
//initialize
int count_array_size=1700000;
int * count_array;
count_array=new int [count_array_size];
for (int i=0; i<count_array_size; i++)
{
count_array[i]=0;
}
//
int general_array_size=reqNr;
TElement ** general_array;
general_array=new TElement *[general_array_size];
for (int i=0; i<general_array_size; i++)
{
general_array[i]= new TElement(req[i],i);
}
//--------------------------------------------------
//counting sort
//count number of each element
for (int i=0; i<general_array_size; i++)
{
count_array[general_array[i]->m_data.m_TimeFrom]++;
}
//modify array to find postiions
for (int i=0; i<count_array_size-1; i++)
{
count_array[i+1]=count_array[i+1]+count_array[i];
}
//make output array, and fill in the sorted data
TElement ** sorted_general_array;
sorted_general_array=new TElement *[general_array_size];
for (int i=0; i <general_array_size; i++)
{
int insert_position=count_array[general_array[i]->m_data.m_TimeFrom]-1;
sorted_general_array[insert_position]=new TElement;
//cout << "inserting " << general_array[i]->m_data.m_TimeFrom << " to " << insert_position << endl;
sorted_general_array[insert_position]->m_data.m_TimeFrom=general_array[i]->m_data.m_TimeFrom;
sorted_general_array[insert_position]->m_data.m_TimeTo=general_array[i]->m_data.m_TimeTo;
sorted_general_array[insert_position]->m_data.m_Channel=general_array[i]->m_data.m_Channel;
sorted_general_array[insert_position]->index=general_array[i]->index;
count_array[general_array[i]->m_data.m_TimeFrom]--;
delete general_array[i];
}
//free memory which is no longer needed
delete [] general_array;
delete [] count_array;
//--------------------------------------------------
int channels_number=AssignChannels(sorted_general_array,general_array_size);
if (channels_number==-1)
{
channels_number=0;
}
else
{
channels_number++;
}
//output
for (int i=0; i<general_array_size; i++)
{
req[sorted_general_array[i]->index]->m_Channel=sorted_general_array[i]->m_data.m_Channel;
}
//free memory and return
for (int i=0; i<general_array_size; i++)
{
delete sorted_general_array[i];
}
delete [] sorted_general_array;
return channels_number;
}
int main ( int argc, char * argv [] )
{
TPhone ** ptr;
int cnt, chnl;
if ( ! (cin >> cnt) ) return 1;
ptr = new TPhone * [ cnt ];
for ( int i = 0; i < cnt; i ++ )
{
TPhone * n = new TPhone;
if ( ! (cin >> n -> m_TimeFrom >> n -> m_TimeTo) ) return 1;
ptr[i] = n;
}
chnl = AllocChannels ( ptr, cnt );
cout << chnl << endl;
for ( int i = 0; i < cnt; i ++ )
{
cout << ptr[i] -> m_Channel << endl;
delete ptr[i];
}
delete [] ptr;
return 0;
}
答案 0 :(得分:4)
这个问题已经有了一个公认的答案。但是我想描述一种与接受的答案略有不同的方法。
您需要衡量
如果不进行测量,您无法告诉任何有关性能的信息。并且衡量我们需要测试用例。所以在我看来,第一项工作是创建一个可以生成测试用例的程序。
我做了一大堆假设,这可能是不正确的,并生成以下代码来生成测试用例:
#include <iostream>
#include <random>
int
main()
{
const unsigned N = 10000000;
std::mt19937_64 eng(0);
std::uniform_int_distribution<unsigned> start_time(0, N);
std::chi_squared_distribution<> duration(4);
std::cout << N << '\n';
for (unsigned i = 0; i < N;)
{
unsigned st = start_time(eng);
unsigned et = st + static_cast<unsigned>(duration(eng));
if (et > st)
{
std::cout << st << ' ' << et << '\n';
++i;
}
}
}
可以改变N
的值,随机数引擎上的播种范围(如果不是随机数引擎的选择),开始时间的范围以及概率的类型/形状持续时间的分布。我凭空掏出了这些选择。您的教授可能对此问题的合理测试用例的生成有更好的想法。但是测量某事比测量什么更好。
使用std::lib
标准库中充满了容器和算法。这个代码不仅被调试,而且效率很高。重复使用此代码是一种很好的编码风格,因为:
std:lib
未提供容器和算法时对其进行编码。std::lib
中错误的概率(尽管概率都不为零)。例如
我使用I / O扩充了您的TPhone
结构,以减轻您在main中执行的I / O的复杂性:
friend
std::istream&
operator>>(std::istream& is, TPhone& p)
{
return is >> p.m_TimeFrom >> p.m_TimeTo;
}
friend
std::ostream&
operator<<(std::ostream& os, const TPhone& p)
{
return os << '{' << p.m_TimeFrom << ", "
<< p.m_TimeTo << ", "
<< p.m_Channel << '}';
}
我选择vector<TPhone>
来接听所有电话。这简化了这一点:
int main ( int argc, char * argv [] )
{
TPhone ** ptr;
int cnt, chnl;
if ( ! (cin >> cnt) ) return 1;
ptr = new TPhone * [ cnt ];
for ( int i = 0; i < cnt; i ++ )
{
TPhone * n = new TPhone;
if ( ! (cin >> n -> m_TimeFrom >> n -> m_TimeTo) ) return 1;
ptr[i] = n;
}
到此为止:
int main()
{
using namespace std;
vector<TPhone> ptr;
int cnt;
if (!(cin >> cnt)) return 1;
ptr.reserve(cnt);
for (int i = 0; i < cnt; ++i)
{
TPhone n;
if (!(cin >> n)) return 1;
ptr.push_back(n);
}
事实证明我的版本比你的版本更有效率。我只是通过学习如何使用std::vector
来获得这种效率&#34;免费#34;
AllocChannels
现在可以使用std::vector<TPhone>&
:
int
AllocChannels(std::vector<TPhone>& ptr)
在这里,我使用了我能想到的最简单的算法。不是因为我认为它可能是最快的,而是因为你需要一个基线来衡量。事实证明,简单并不总是很慢......
int
AllocChannels(std::vector<TPhone>& ptr)
{
using namespace std;
if (ptr.size() == 0)
return 0;
// sort TPhone's by x.m_TimeFrom
sort(ptr.begin(), ptr.end(), [](const TPhone& x, const TPhone& y)
{
return x.m_TimeFrom < y.m_TimeFrom;
});
// Create channel 0 and mark it as busy by the ptr[0] until ptr[0].m_TimeTo
vector<unsigned> channels(1, ptr.front().m_TimeTo);
ptr.front().m_Channel = 0;
// For each call after the first one ...
for (auto i = next(ptr.begin()); i != ptr.end(); ++i)
{
// Find the first channel that isn't busy at this m_TimeFrom
auto j = find_if(channels.begin(), channels.end(),
[&](unsigned tf)
{
return tf < i->m_TimeFrom;
});
if (j != channels.end())
{
// Found a non-busy channel, record it in use for this call
i->m_Channel = j - channels.begin();
// Mark the channel busy until m_TimeTo
*j = i->m_TimeTo;
}
else
{
// Record a new channel for this call
i->m_Channel = channels.size();
// Create a new channel and mark it busy until m_TimeTo
channels.push_back(i->m_TimeTo);
}
}
return channels.size();
}
我使用了一些C ++ 11功能,因为它们很方便(例如auto和lambdas)。如果您没有这些功能,那么它们很容易在C ++ 03中解决。我使用的基本算法是按m_TimeFrom
排序,然后在排序的调用列表中进行线性遍历,并且对于每个调用,通过一组通道进行线性搜索,寻找一个不使用(如果所有都在使用中,则创建一个新的)。请注意使用标准算法sort
和find_if
。没有意义重新实现这些,特别是对于基线测试用例。
我使用<chrono>
来计算所有内容:
auto t0 = chrono::high_resolution_clock::now();
int chnl = AllocChannels(ptr);
auto t1 = std::chrono::high_resolution_clock::now();
我以完全相同的方式检测您的代码,以便我可以测试它们。以下是我的结果,首先生成一个长度为= {100,1000,10000,100000,1000000,10000000}的测试用例,并且对于每个长度,首先运行您的代码,然后使用此输出:
cout << "#intervals = " << cnt << '\n';
cout << "#channels = " << chnl << '\n';
cout << "time = " << chrono::duration<double>(t1-t0).count() << "s\n";
这是我得到的:
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 100
#channels = 10
time = 0.00565518s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 100
#channels = 10
time = 6.934e-06s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 1000
#channels = 17
time = 0.00578557s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 1000
#channels = 17
time = 5.4779e-05s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 10000
#channels = 16
time = 0.00801314s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 10000
#channels = 16
time = 0.000656864s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 100000
#channels = 18
time = 0.0418109s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 100000
#channels = 18
time = 0.00788054s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 1000000
#channels = 19
time = 0.688571s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 1000000
#channels = 19
time = 0.093764s
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out > test.dat
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
Segmentation fault: 11
$ clang++ -stdlib=libc++ -std=c++11 test.cpp -O3
$ a.out < test.dat
#intervals = 10000000
#channels = 21
time = 1.07429s
<强>摘要强>
这里没有人,包括我自己,预测最简单的解决方案会以惊人的数量始终击败你的第一次尝试。这可能是我生成的测试用例的一个特征。通过生成其他测试用例进行测量,这将是您学习的其他内容。
我不知道N = 10000000的案例的分段错误的原因。我没有花时间研究你的代码。坦率地说,我觉得你的代码很复杂。
我忽略了写一个正确性测试。那应该是我的第一步。输出是否正确?我懒得,只是瞥了一下N == 100的案子,看看它是否正确。
由于重复使用了std :: containers和算法,我的代码实际上比你的代码更容易调整性能。例如,您可以尝试std::lower_bound
(二分搜索)代替std::find_if
,衡量,如果它有所改善(我不打赌,但是你)应该衡量,并使用你尊重的test.dat
。
将代码分解为容器和算法。重复使用std定义的容器和算法,否则创建自己的容器和算法,可以在将来的编码中重复使用。作为一名学生,我希望std定义的那些适用于大多数用例。
始终测试正确性(因为我在这里没有这样做:-))不要在没有测量的情况下假设任何关于性能的事情。二进制搜索并不总是比线性搜索更快,即使它具有更好的渐近复杂度。输入数据会强烈影响算法的性能。了解如何生成各种输入数据,以了解您的算法如何受到影响。 <random>
非常适合这项任务。
答案 1 :(得分:2)
将条目存储在std::vector<TPhone>
而不是TPhone **
中。这将在内存中连续布局连续的TPhone
个对象,从而减少缓存未命中。
为unsigned int
的成员试用TPhone
以外的其他数据类型。有关您可以尝试的类型,请参阅<cstdint>
。
答案 2 :(得分:0)
设[a i ,b i )为你的间隔,i = 1,...,n。您想设计一个函数通道(i),它返回每个n个区间的通道号。
您唯一的限制是没有两个相交的间隔可以在同一个通道上。这对应于一个无向图,其中您的间隔是顶点,当且仅当相应的间隔相交时,两个顶点之间有一条边。
如果这些顶点形成independent set,您可以将通道C分配给特定的一组顶点(间隔)。
您希望找到一组此表单的独立集合,其中所有表单的并集覆盖图形,并且它们是成对不相交的。你想要尽可能少的独立集。
找到最大独立集的(相关)问题是NP完全的。所以我认为你不应该期望找到一个多项式时间算法来找到给你最小通道数的解决方案。
更现实的期望有以下两种形式之一:(A)花费超多项式时间来解决问题,或者(B)使用可能无法为您提供全局最优的近似算法。
对于(B)你可以这样做:
feasible(M)
initialize M empty channels (lists of intervals)
sort intervals by a_i value
for each interval I = [a_i, b_i):
insert it into the channel for which the most recent
interval is closest to the current interval (but not intersecting)
if I cannot be inserted at the end of any channel, return false
return true //the M channels are a feasible solution
现在使用此程序,您可以指数搜索可行的最小M,返回true。
尝试M = 1,2,4,8,16 ......直到你遇到第一个M = 2 k ,使得feasible(M)
返回true。然后在2 k - 1 和2 k 之间进行二元搜索,找到最小值M.
答案 3 :(得分:0)
如果您对集合进行了排序,为什么要使用线性搜索?使用二进制搜索。
答案 4 :(得分:0)
如果您希望算法快速,则应尽可能减少搜索。此外,您不需要知道哪些区间被“链接在一起”以确定每个区间的正确信道(即,不使用比绝对必要的更多信道)。以下是我将用于获得最佳性能的步骤/技术:
像这样定义你的间隔类,添加两个内联函数定义(我使用TimeDescriptor的结构只是风格问题,而不是这段代码完全是时尚的):
typedef struct TimeDescriptor {
unsigned time;
bool isEnd;
} TimeDescriptor;
class TimeInterval {
public:
TimeDescriptor start, end;
unsigned channel;
TimeInterval(unsigned startTime, unsigned endTime) {
start = (TimeDescriptor){ startTime, false };
end = (TimeDescriptor){ endTime, true };
}
}
inline TimeInterval* getInterval(TimeDescriptor* me) {
return (me->isEnd) ? (TimeInterval*)(me - 1) : (TimeInterval*)me;
}
inline TimeDescriptor* getOther(TimeDescriptor* me) {
return (me->isEnd) ? (me - 1) : (me + 1);
}
创建一个指向所有TimeDescriptors的指针数组,每个TimeInterval两个指针(一个用于开始,另一个用于结束)。
按时间排序此TimeDescriptor指针数组。确保使用isEnd
标志作为辅助排序键。我不确定如何定义区间碰撞,i。即两个间隔(20,30)和(30,40)是否发生冲突,如果它们发生冲突,在开始时间之后用相同的值对结束时间进行排序,如果它们没有冲突,则在开始时间之前用相同的值对结束时间进行排序
无论如何,我建议只使用标准的快速排序实现来对阵列进行排序。
为未使用的频道号创建堆栈。关于这个堆栈的重要事项是:它必须允许你在恒定时间内获取/推送一个通道号,理想情况是通过在内存中更新不超过两个数字;它必须是无底的,我。即它必须允许你弹出任意数量的值,产生一个递增的整数序列。
实现这种堆栈的最简单方法可能是编写一个使用std::vector<unsigned>
存储空闲通道的小类,并跟踪所用的最大通道数。无论何时无法从内部存储器处理弹出请求,都会通过将最大通道编号递增1来生成新的通道编号。
遍历已排序的TimeDescriptors数组。每次遇到开始时间时,都会获取一个通道编号,并将其存储在相应的TimeInterval中(使用getInterval()
)。每次遇到结束时间时,请将其频道编号推回到空闲频道阵列。
当您通过时,您的免费频道堆栈将告诉您同时使用的最大频道数,并且每个TimeInterval将包含要使用的正确频道号。您甚至可以通过简单地通过通道号来使用TimeInterval数组来有效地计算共享通道的所有区间链...
答案 5 :(得分:0)
很抱歉在这里成为一名死灵法师,但在阅读了问题并发布了答案之后,我真的不能放过这个。
这个问题有一个非常有效的贪婪解决方案。考虑以下递归:
这将产生最佳通道数。通过间隔数量的归纳,证明是微不足道的。
速度的关键在于如果没有冲突&#34;。这意味着将对已处理的内容和剩余的内容进行比较,并且应该很容易说服自己,首先对输入进行排序比在处理过程中对它们进行排序要快得多(或不对它们进行排序)。
如果您不相信,请考虑以下两个极端:
我们需要按开始,然后结束时间对输入进行排序。如果我们选择稳定的排序,并且首先按结束时间排序,然后按开始时间排序,这很容易。如果所有值都是整数,则Counting Sort的稳定版本可能是最佳选择;输入数量远远大于输入范围;和内存使用不是一个重要的考虑因素。在这些条件下,这种排序是线性的。
通过对输入进行排序,我们只需将每个间隔与分配给每个通道的最后一个间隔进行比较。在没有间隔重叠的极端情况下,该算法是线性的:O(n)排序,+ O(n)处理= O(n)。在所有区间重叠的极端的另一端,没有进一步改进,算法将是二次的。
为了改善这一点,而不是与所有频道进行比较,如果频道按最早的结束时间排序,那么与第一个频道的冲突将自动指示所有频道发生冲突。然后,对于每个区间,我们只需要进行1次比较,以及维持通道的排序顺序所需的一切。
为此,我建议将频道保持在最小堆中(按结束时间)。比较所需的通道始终位于顶部。看看那个频道,然后:
在最糟糕的噩梦场景中,排序的间隔将具有单调增加的开始时间,并且单调减少结束时间。这给了我们算法的最坏情况O(n + lg 1 + lg 2 + ... + lg n)= O(n + lg(n!))= O(n + n lg n)= O (n lg n)
渐近更好并不总是更好。它实际上取决于输入的分布以及输入的大小。我确信这里描述的算法优于其他算法,但是在渐进相同的选择的实现中肯定存在空间,但会产生不同的结果。