Skip to content

Instantly share code, notes, and snippets.

@fotile96
Created February 7, 2025 04:59
Show Gist options
  • Save fotile96/4b617d65922e648a05c48923ada400ed to your computer and use it in GitHub Desktop.
Save fotile96/4b617d65922e648a05c48923ada400ed to your computer and use it in GitHub Desktop.
yet another quick selection problem by r1
帮我用C++解决下面的问题:有n个double pair (a[i], b[i]),其中保证b[i]都是严格正的,我们希望首先将它们按a升序排列,并记排序后的b数组的部分和(partial sum)为B。容易看到B也是一个严格单调上升的序列。给定某个x,我们希望找到最大的i使得B[i]<=x。
为了得到O(n)而非O(nlogn)的总时间复杂度的解法,你可能可以先考虑如何实现下面一般的算法,它总体上类似于随机化的快速选择算法(quick selection algorithm),假设输入为一列n个具有全序关系的元素,且已经被random shuffle过,每次挑出一个pivot元素,在线性时间内在当前递归子区间内就地将元素移动成,在pivot元素左边的元素都小于pivot,在它右边的元素都大于它。与经典的快速选择算法的区别在于,我们希望支持传入一个回调函数来判断应该在pivot元素的哪一侧继续递归处理,而非通常的通过事先固定的某个名次k来判断。
最初的问题应当可以通过设计一个合适的callback判断函数,并调用这个快速选择算法变体来求解。不过你不需要非常严格地模块化地实现整个程序,把这个具体的callback hardcode在递归函数体里也是可以接受的。
CoT
=====
好的,我现在得仔细看看这个问题。题目是说,给定n个double对(a[i], b[i]),其中每个b[i]都是严格正的。我们需要先把这些对按a升序排列,然后计算排序后的b数组的部分和B。因为b都是正的,所以B是严格递增的。然后给定一个x,要找出最大的i使得B[i] <=x。要求用O(n)的时间复杂度,而不是O(n log n)。所以常规的排序然后遍历的方法不行,因为排序是O(n log n)的。
那问题的关键在于如何在不完全排序的情况下找到满足条件的i的最大值。这时候想到可能需要类似快速选择算法的方法,因为快速选择可以在平均情况下O(n)的时间找到第k小的元素。不过这里的情况可能更复杂,因为我们需要基于部分和的条件来找最大的i。
题目提示说要类似随机化的快速选择算法,但回调函数来决定在pivot的哪边继续处理。这可能意味着在每次选择一个pivot后,根据部分和的条件决定继续处理左边还是右边。可能需要动态计算部分和,并且在递归过程中维护这些信息。
首先,我需要明确,当我们按a升序排列之后,每个元素的位置会影响它们的b的顺序。而部分和B[i]是前i+1个元素的和。例如,B[0] = b[0], B[1]=b[0]+b[1],以此类推。所以问题转化为,找到最大的k,使得前k+1个元素的和不超过x。
但问题是,我们需要在未排序的情况下找到这样的k,或者更准确地说,找到在排序后的数组中满足条件的最大i。所以直接排序可能不行,因为排序的时间复杂度太高。
这时候,可能需要使用类似于快速选择的方法,在每一步选择一个pivot,将数组分成两部分,左边的a都小于等于pivot的a,右边的大于。然后计算左边的部分和总和,如果总和小于等于x,那么可能答案在左边加上右边可能需要继续处理的部分,或者相反?
但这里可能需要更巧妙的处理方式。假设在某个步骤中,我们选择一个pivot,将所有a小于等于pivot的元素放到左边,剩下的在右边。然后计算左边这些元素的b的总和sum_left。如果sum_left <=x,那么说明这些左边的元素的总和已经被包含,可能可以保留这些元素,并且继续在右边寻找可能的更大的元素,但右边的a更大,所以排序后的顺序中,它们的b会被加到后面。这时候,如果sum_left +右边的部分和可能超过x的话,那可能需要递归处理右边。或者,如果sum_left已经超过x,那么答案只能在左边更小的元素中寻找。
不过这里可能需要更详细的思考。假设我们已经将数组划分为左右两部分,左边是所有a<=pivot_a的元素,右边是a>pivot_a的元素。在排序后的数组中,左边部分会排在右边部分的前面。因此,左边元素的b之和的总和可能被计入到部分和中。如果左边元素的总和sum_left <=x的话,那么可能可以保留左边的所有元素,并在右边继续寻找可能的更大的元素,使得sum_left加上右边的部分和的总和不超过x。否则,如果sum_left >x,那么答案只能在左边的元素中,但此时需要继续在左边中找到最大的子集,使得它们的总和不超过x。
或者,这可能涉及到动态地维护候选集的总和,并根据总和与x的关系来决定如何处理左右两部分。
不过,这可能比较复杂。因为每次划分后,左边和右边的元素的a的顺序是确定的(左边的a<=pivot,右边的a>pivot),因此在最终的排序数组中,左边元素全部在右边元素的前面。所以他们的b的顺序在部分和中是按整个左边的总和,然后是右边的各个元素的总和逐步增加的。
因此,假设我们在某个递归步骤中,有一个候选集合的元素,其中一部分已经被确定必须包含在最终的答案中,另一部分可能需要处理。例如,当我们处理一个pivot时,左边的sum_left如果总和<=x,那么可以将这些元素全部包含进去,并继续处理右边,因为右边可能有元素的a更大,在排序后的数组中会被排在后面,所以他们的b会被加到总和之后。因此,假设右边的元素中可能有部分可以被包含进来,使得总和仍然不超过x。此时,问题转化为在右边的元素中找到最大的k,使得sum_left加上这些元素的部分和不超过x。
这可能类似于分治法的处理方式。例如,在每次递归时,处理一个子数组,并维护当前的累积总和。例如,当处理一个子数组时,我们选择pivot,将元素分为左右两部分。左边的所有元素的a都<=pivot的a。计算左边元素的总和sum_left_part。如果当前累积总和加上sum_left_part <=x,那么这些元素可以全部被选中,累积总和加上sum_left_part,然后递归处理右边的部分,看看是否还能添加更多的元素。否则,如果累积总和加上sum_left_part >x,那么需要在左边的部分中找到最大的子集,使得累积总和加上其部分和不超过x。
但这样的话,递归过程中需要维护一个累积的总和,即之前已经确定的元素的总和。例如,假设在之前的递归步骤中已经选中的元素总和是current_sum。当处理当前子数组时,如果左边的sum_left_part的current_sum加上它之和仍小于等于x,那么可以将这些元素全部加入,current_sum += sum_left_part,然后在右边的子数组中继续处理。否则,需要在左边子数组中寻找更小的子集。
这可能需要递归函数的参数中包含current_sum的值,并且每次处理子数组时,根据current_sum和sum_left_part的关系决定如何处理。
这可能是一个可行的方法。那具体的步骤可能如下:
1. 随机打乱数组(这一步可能需要,以避免最坏情况的时间复杂度)。
2. 定义一个递归函数,该函数处理数组的某个区间[l, r],并维护current_sum,以及当前可能的最大元素数目count。函数的目的是在该区间中找到最大的k,使得current_sum加上该区间中前k个元素的部分和不超过x。返回的是最大的k和对应的总和(或者累计总和)。
或者,可能需要另一种结构:在递归处理某个子数组时,计算该子数组中所有元素的sum,并判断是否可以被全部包含。如果可以,则将这些元素全部加入,然后继续处理其他可能的元素。否则,需要将该子数组划分为更小的部分,继续处理。
但这种方法可能需要多次划分,并且如何维护总和是关键。
例如,假设我们有一个数组区间,选择pivot后,将其分为左边和右边。左边所有元素的a<=pivot的a。计算左边的sum_left。如果current_sum + sum_left <=x,那么说明左边全部可以加入,current_sum += sum_left,然后在右边的区间中继续处理。否则,左边的部分可能存在部分元素可以被加入,此时需要在左边区间递归处理,而右边的元素的a较大,无法被包含(因为左边元素的a较小,在排序后的数组中排在前面,所以右边的元素的b之和只能在左边之后加入。因此,如果左边部分的总和已经超过了x,那么右边部分的元素无论是否加入都不影响,因为左边的总和已经超过x,所以只能选择左边的一部分)。
这可能类似于快速选择中的处理方式,但需要根据sum_left和x的关系来决定递归的方向。
那么,递归函数的步骤可能如下:
函数参数:数组的区间[l, r],以及current_sum。
函数的返回值:最大的i,使得current_sum加上该区间中前i+1个元素的部分和不超过x。同时返回对应的总和current_sum + sum_of_selected_elements。
或者,函数可能返回两个值:最大的数目k,和对应的总和。例如,当处理区间[l,r]时,计算该区间中的元素能选出的最多元素数目k,使得总和不超过x - current_sum。然后返回k和sum_of_k_elements。这样,主函数就可以累加这些值。
这可能比较复杂,但或许可以这样处理。
举个例子,假设我们有整个数组需要处理。current_sum初始为0。我们随机选一个pivot,将数组划分为左边和右边。左边是a<=pivot的元素,右边是a>pivot的元素。左边的sum_left是这些元素的b之和。假设current_sum + sum_left <=x,那么说明左边所有元素都可以被选中,current_sum += sum_left,然后递归处理右边部分。右边部分的元素的a更大,在排序后的数组中的位置更靠后,所以它们的部分和是后续的。因此,此时需要计算右边的部分中可以选多少元素,使得current_sum加上这些元素的部分和不超过x。此时,递归处理右边的区间,传入current_sum的值。右边处理完毕后,返回左边元素的数量加上右边处理后的数量。否则,如果current_sum + sum_left >x,那么说明左边的元素不能全部被选中,必须从中选出部分元素。此时,需要递归处理左边区间,传入current_sum的值,然后在左边得到的数目就是最终结果的一部分。
但如何计算左边元素中最多可以选多少,使得总和不超过x - current_sum?
这可能需要将问题分解为:在左边的元素中,按a升序排列后的部分和最多选到某个k,使得总和不超过x - current_sum。因此,在递归处理左边时,需要将current_sum作为参数,并返回最大的k和对应的sum。
这似乎会导致递归的嵌套结构,类似于分治法,每次将问题划分为子问题,并根据sum_left的情况决定处理哪边。
这样的算法的时间复杂度是否是O(n)呢?因为每次递归处理的子数组的大小是期望的O(n/2),所以总的时间复杂度类似于快速选择的平均情况,即O(n)。但是,这需要每次划分的线性时间处理。
现在,如何具体实现这个递归函数?
可能的步骤:
1. 在当前的区间[l, r]中,随机选择一个pivot元素,将数组划分为左边(a<=pivot的a)和右边(a>pivot的a)。这一步可以在O(r-l+1)的时间内完成,类似于快速排序的partition过程。
2. 计算左边所有元素的b的总和sum_left。
3. 比较current_sum + sum_left与x:
a. 如果current_sum + sum_left <=x:说明左边所有元素可以被选中。此时,current_sum += sum_left。然后递归处理右边的区间,传入current_sum的当前值,并得到右边中可以选中的元素数目k_right。总的结果是左边的元素数目(即左边的大小)加上k_right。
b. 否则:说明左边元素不能全部被选中。此时,需要递归处理左边的区间,传入current_sum的值,得到k_left。总的结果是k_left。右边元素的a更大,所以它们的部分和出现在左边之后,所以如果左边都不能全部选中,右边的更不可能被选中,因为它们的b之和会增加总和。因此,右边不需要处理。
这样,每次递归调用处理的是左边或右边的子数组,并且每个元素只会被处理常数次,类似于快速选择。
那么,这样的递归过程的时间复杂度期望是O(n),因为每次划分的时间是线性的,而递归的层数是期望的O(log n)。
那问题是如何实现这个划分过程,并且在每个划分步骤中计算左边的sum_left。
现在,具体到代码实现:
首先,将整个数组随机打乱,以避免最坏情况的时间复杂度。例如,可以使用Fisher-Yates shuffle算法。
然后,编写一个递归函数,参数是数组的起始和结束索引,以及current_sum的引用,x的值。或者,可以返回两个值:最大的k和对应的sum。
例如,函数可能如下:
int solve(vector<pair<double, double>>& arr, int l, int r, double& current_sum, double x) {
if (l > r) return 0;
// 随机选择pivot,例如中间的元素,或者随机选一个
int pivot_idx = partition(arr, l, r);
// partition函数将arr的[l..r]区间划分为左边a <= pivot的a,右边a> pivot的a,返回pivot的最终位置?
// 或者更准确地说,partition函数类似于快速排序中的划分,将数组分成左边a <= pivot_a,右边a> pivot_a,并返回左边最后一个元素的索引?
// 可能需要自己实现partition函数,该函数返回左边部分的结束位置(比如,左边的元素都<= pivot_a,右边的>)
// 在划分过程中,计算左边所有元素的b之和sum_left
// 所以,可能需要修改partition函数,使其在划分的同时计算sum_left
double sum_left = 0;
int m = partition(arr, l, r, sum_left); // m是左边部分的最后一个元素的索引,即左边是[l..m]
if (current_sum + sum_left <= x) {
current_sum += sum_left;
int left_count = m - l + 1;
int right_count = solve(arr, m+1, r, current_sum, x);
return left_count + right_count;
} else {
int left_count = solve(arr, l, m, current_sum, x);
return left_count;
}
}
这里的关键是,partition函数需要同时返回划分后的分界点m,以及左边元素的b之和sum_left。
如何实现这样的partition函数?
可能的实现方式:
在partition过程中,我们随机选择一个pivot索引,然后将其交换到数组的末尾,或者某个位置。然后维护一个指针i,表示左边部分的末尾。遍历数组,将a小于等于pivot_a的元素交换到i的位置,并累加它们的b值到sum_left中。例如:
int partition(vector<pair<double, double>>& arr, int l, int r, double& sum_left) {
// 随机选择pivot索引
int pivot_idx = l + rand() % (r - l + 1);
double pivot_a = arr[pivot_idx].first;
// 将pivot交换到末尾
swap(arr[pivot_idx], arr[r]);
sum_left = 0;
int i = l - 1; // 左边区域的末尾
for (int j = l; j < r; j++) {
if (arr[j].first <= pivot_a) {
i++;
swap(arr[i], arr[j]);
sum_left += arr[i].second;
}
}
// 将pivot交换到i+1的位置
swap(arr[i+1], arr[r]);
sum_left += arr[i+1].second;
return i + 1; // 左边部分是 [l..i+1]?
// 左边元素的a都<= pivot_a,右边元素a> pivot_a?
}
但可能需要重新检查。例如,当遍历j从l到r-1时,当元素j的a<=pivot_a时,将其交换到i的位置。这样,i的左边都是<=pivot_a的元素。最后,将pivot(位于r的位置)交换到i+1的位置,此时左边是[l..i],pivot在i+1的位置,右边是i+2到r的元素?或者可能我的逻辑有错误?
或者,可能sum_left的计算方式有问题。例如,当遍历时交换元素到左边,sum_left需要累加这些元素的b值。例如,当交换arr[j]到i的位置时,此时的元素是arr[i],其b值应该被加到sum_left中。这可能正确,因为所有被交换到左边的元素的a都<=pivot_a,所以他们的b值都会被计入sum_left。
这样,整个左边部分的sum_left即为这些元素的b的总和。那partition函数返回的m是i+1,此时左边部分是[l..i+1 -1]即i的位置?或者,例如,返回的m是左边部分的最后一个元素的索引?
可能需要更仔细的检查。
比如,假设pivot被交换到r的位置。遍历j从l到r-1。当元素j的a <= pivot_a时,交换到i+1的位置,并i++。循环结束后,i指向最后一个满足条件的元素的位置。然后交换pivot到i+1的位置。此时,左边部分是[l..i+1]的元素的a<=pivot_a?而右边部分是i+2到r的元素?
或者可能这个逻辑需要重新分析。例如,假设初始时i=l-1。然后对于每个j从l到r-1:
如果arr[j].a <= pivot_a,则i++,交换arr[i]和arr[j]。这样,所有交换到i左边的元素(包括i的位置)的a都<= pivot_a。遍历结束后,i的位置是最后一个满足条件的元素。然后交换arr[r](pivot)到i+1的位置。此时,左边部分(<= pivot_a)的元素是[l..i+1],右边是[i+2..r]。
所以,在partition函数中,sum_left等于这些元素的b的总和,即左边部分[l..i+1]的元素的b之和?
是的,因为在遍历过程中,每当j位置的元素满足条件时,就被交换到i的位置(此时i已经自增),所以交换后的arr[i]的b会被加到sum_left中。然后,最后交换pivot到i+1的位置,此时sum_left需要加上arr[i+1].second的值?
那这样,在遍历完成后,sum_left已经累加了所有被交换到左边元素的b,包括i的位置的元素。然后将pivot交换到i+1的位置,此时sum_left还需要加上pivot的b。是的,因为pivot的a等于pivot_a,所以它会被包含在左边部分。
所以,sum_left的正确性应该得到保证。
那这样,partition函数返回i+1的位置,即左边部分的最后一个元素的索引。
现在,回到递归函数中,当处理区间[l, r],选择pivot,划分成左边[l..m]和右边[m+1..r]。左边的sum_left是这些元素的b的总和。然后判断current_sum + sum_left <=x。如果是的话,说明这些元素都可以被选中,所以current_sum累加sum_left,然后递归处理右边[m+1..r],并将右边的返回结果加上左边的元素数目(m - l +1)。
否则,说明左边的sum_left太大,无法全部选中。此时,递归处理左边的区间[l..m],寻找能选中的最大数目,右边的元素不会被选中,因为它们的a更大,所以在排序后的数组中,它们的部分和出现在左边之后。因此,如果左边的总和已经超过x,那么右边的元素无论如何都不可能被选中,因为它们会被加在总和之后,导致总和更大。
这样递归下去,最终就能得到最大的i,使得总和不超过x。
那现在的问题是,这样的算法是否正确?
举个例子,假设所有元素的a已经按升序排列。此时,每次递归处理都会将整个数组划分为左边和右边。假设sum_left的总和加上current_sum <=x,那么整个左边被选中,右边递归处理。这样,最终的总和就是整个数组的部分和,返回的总元素数目就是整个数组的长度。
这显然正确。
另一个例子,假设x非常小,只能选第一个元素。那么,在递归过程中,每次划分左边可能总和超过x,因此递归处理左边。最终当数组被划分到单个元素时,如果它的b的值小于等于x,则返回1,否则0。
这似乎正确。
那现在需要测试这样的算法是否正确。
例如,假设n=3个元素:
元素按照排序后的a的顺序是 a1 < a2 <a3,对应的b为1, 2,3。总和B数组是1,3,6。如果x=4,那么最大的i是1,因为B[1]=3 <=4,而B[2]=6>4。所以答案是i=1。
假设初始数组是乱序的,比如元素的顺序是a3,a1,a2。此时,第一个pivot可能选a1。划分左边是a1,右边是a3,a2。sum_left=1。current_sum是0。0+1=1 <=4,所以current_sum变为1。然后递归处理右边部分:a3,a2。右边的sum_left需要根据划分后的sum_left的值。假设在右边的处理中,选择pivot为a2。划分左边是a2,右边是a3。sum_left=2。current_sum是1+2=3 <=4。所以current_sum变为3。然后处理右边的a3。sum_left=3。1+3+3=7>4,所以无法全部选中。递归处理左边的a3?或者此时右边的处理中的sum_left是3吗?可能在这里出现错误。
或者,可能这个例子中的处理流程是否正确?
这可能比较复杂。需要详细分析。
假设初始数组是乱序的,例如:
原始数组的元素是 (a3,3), (a1,1), (a2,2),其中a3> a1> a2?
或者,假设a的顺序是a3> a2> a1?
抱歉,可能例子需要重新构造。
假设原始数组的元素是:
元素0:a=3,b=3
元素1:a=1,b=1
元素2:a=2,b=2
那么排序后的顺序是按a升序排列:元素1(a=1),元素2(a=2),元素0(a=3)。对应的B数组是1,1+2=3,3+3=6。所以,当x=4时,最大的i是1(B[1]=3 <=4,而B[2]=6>4)。
现在,在初始的处理中,整个数组是[0,1,2],即元素是(3,3)、(1,1)、(2,2)。假设第一次调用solve处理区间[0,2],current_sum=0。
第一次的pivot随机选择,假设选择元素1(即索引1)的a=1作为pivot。划分后的左边是a<=1的元素,即只有元素1。sum_left=1。current_sum+sum_left=1<=4,所以current_sum变为1。左边数目是1。递归处理右边区间[0,2],即剩下的元素0和2?
或者,原来的数组在划分后的结构是怎样的?
可能此时,在第一次partition调用中,处理整个数组。选择pivot元素为元素1(a=1),将其交换到末尾。此时数组变为元素0、元素2、元素1。然后遍历j从0到1(r=2,所以r-1=1):
j=0:元素0的a=3 >1,所以不交换。
j=1:元素2的a=2>1,不交换。
循环结束。此时i的初始值是-1,所以循环结束后i仍然是-1。然后将pivot(元素1)交换到i+1=0的位置。数组变为元素1(a=1)、元素2(a=2)、元素0(a=3)。sum_left的值是元素1的b=1。
所以,左边的区间是[0..0],即元素1。右边是[1..2],即元素2和0。
此时,current_sum=0+sum_left=1 <=4,所以递归处理右边区间[1..2],current_sum=1。
右边的处理中,传入的区间是元素2(a=2,b=2)和元素0(a=3,b=3)。
现在,处理右边的区间[1..2]。随机选择pivot,比如选择元素2(索引1,a=2)。划分后的左边是元素2,因为其a<=2。sum_left=2。current_sum=1+2=3 <=4。所以current_sum变为3。左边数目是1(元素2)。递归处理右边区间[2..2](元素0)。
处理右边的区间[2..2],即元素0的a=3。此时sum_left=3。current_sum+3=6>4。所以无法全部选中,递归处理左边的区间,即元素0所在的区间。此时区间只有一个元素。sum_left=3> x-current_sum=4-3=1。因此,无法选中这个元素。返回0。所以右边的递归处理返回0。总的结果是左边的数目(元素2的数目1)加上0。所以总共有1+1+0=2?这显然不对,因为正确的i是1,而这里返回的是左边的数目1(元素1)加上右边的数目1(元素2),即总共有2个元素,对应的总和是1+2=3,此时总和3<=4。而第三位的总和是3+3=6>4。所以正确的答案是i=2?或者原题中的i是0-based吗?
哦,原题中的i的索引可能对应B数组的索引。比如,B[0]是b[0],B[1]是b[0]+b[1],等等。所以,当选中两个元素时,他们的总和是3,对应的i是1。所以正确的答案应该是i=1。而根据上述处理,总返回的数目是2,导致i=1(数目是2,对应的i是数目-1=1)。所以是正确的?
此时,递归函数返回的总数目是元素1和元素2的数目,即2,所以最大的i是1。这与正确结果一致。
那此时,算法是否返回正确的结果?是的。
那这说明算法是正确的?
那另一个例子,假设x=3。正确的结果是i=1,对应的总和3。算法的处理是否如此?
第一次划分,sum_left=1,current_sum=1,处理右边元素2和0。sum_left=2,current_sum+2=3<=3。处理右边元素0,sum_left=3。current_sum=3+3=6>3。无法选中元素0。所以递归处理左边的元素0的区间,返回0。总数目是1(元素1)+1(元素2)+0=2,即i=1。正确。
那这说明算法是正确的。
那现在如何实现这个算法?
需要先将数组随机打乱。然后调用solve函数。
主函数的步骤:
1. 输入n,数组的元素。
2. 将数组随机打乱。
3. 初始化current_sum为0。
4. 调用solve函数处理整个数组,返回最大的数目k,使得总和不超过x。最大的i是k-1。
或者,solve函数返回的是选中的元素数目,因此i是数目-1。例如,如果数目是2,则对应的i是1。
例如,在上面的例子中,返回数目2,对应的i是1。正确。
因此,主函数需要返回solve的结果减一。
所以,最终的答案应该是solve函数返回的数目k,那么最大的i是k-1。或者,是否在计算的时候数目是i+1?比如,当选中k个元素时,最大的i是k-1。
是的。比如,数目为k的元素对应的B[k-1] = sum of first k elements.所以,最大的i是k-1.
所以,主函数中的solve返回k,那么答案i是k-1。
或者,要看函数solve返回的是元素数目,比如数目是k,那么最大的i是k-1。例如,数目为2,则i=1。
所以,在代码中,solve函数返回的是k,那么最终的i是k-1。但是如果数目是零,则返回-1?或者题目中是否有保证x>=0?
题目中给出x是一个给定的值,但原题中的i的最大可能为n-1。如果x比所有B[i]小,则返回最大的i是最大的满足B[i] <=x的。比如,如果x< B[0],那么无法选中任何元素,返回-1?
这需要处理边界条件。例如,当没有元素的总和<=x时,返回-1。
所以,在主函数中,首先调用solve函数得到k,即选中的元素数目。如果k>=0,则返回k-1,否则返回-1?
或者,solve函数返回k是选中的元素数目,其中k可以是0到n。例如,当x足够大,可以选中所有元素,那么k=n,i=n-1。如果x < B[0],则k=0,返回i=-1。
这样,主函数的答案应该是k==0 ? -1 : k-1。或者,如果k=0,则说明没有元素被选中,返回-1。否则返回k-1。
因此,整个流程的代码大概如下:
首先,实现随机打乱数组:
可以用std::shuffle,或者手动实现。
然后,实现partition函数,返回划分点和sum_left。
然后,实现递归函数:
int solve(vector<pair<double, double>>& arr, int l, int r, double& current_sum, double x) {
if (l > r) return 0;
double sum_left;
int m = partition(arr, l, r, sum_left);
if (current_sum + sum_left <= x) {
current_sum += sum_left;
int left_count = m - l + 1;
int right_count = solve(arr, m + 1, r, current_sum, x);
return left_count + right_count;
} else {
int left_count = solve(arr, l, m - 1, current_sum, x);
// 或者是否应该是solve(arr, l, m, current_sum, x)? 因为m是左边部分的最后一个元素?
// 原来的划分中,左边是[l..m]吗?
// 在之前的partition函数中,返回的m是左边部分的最后一个元素的索引,所以左边区间是[l..m]。所以当递归处理左边时,应该是l到m。
int left_count = solve(arr, l, m, current_sum, x);
return left_count;
}
}
这样,在递归处理左边的时候,传入的区间是[l..m],而不是[m+1..r]。
比如,在之前的例子中,划分后的左边是[l..m],右边是[m+1..r]。
那,在递归处理左边的情况下,应该调用solve(arr, l, m, ...),而不是m-1。
所以,在代码中,当处理左边时,传入的区间是[l..m]。这可能正确。
那,在代码中:
在递归处理左边的时候,应该调用solve(arr, l, m, current_sum, x),因为左边是[l..m]。
那修改后的代码:
else {
int left_count = solve(arr, l, m, current_sum, x);
return left_count;
}
这样,当左边的sum_left超过x - current_sum时,递归处理整个左边区间[l..m]。
这可能正确。
那之前的例子中,当sum_left=1,current_sum=0,sum_left<=x=4,处理右边。此时,右边区间是[m+1..r]。例如,m是0,所以右边是1到2。
这可能正确。
那现在,在递归函数中,参数是当前处理的区间l和r。在每次调用时,该区间会被处理。
那么,现在测试另一个例子:
假设数组中的元素是(a=2,b=3),(a=1,b=1),x=3.
排序后的数组按a顺序是(1,1),(2,3)。B数组是1,4。x=3的话,最大的i是0,对应的B[0]=1 <=3, B[1]=4>3。所以答案i=0.
假设初始数组是元素顺序是(2,3)、(1,1)。
在第一次调用时,处理区间0-1。随机选择pivot,比如选索引1(a=1)。划分后的左边是元素1,sum_left=1。current_sum=0+1=1 <=3。递归处理右边的区间m+1=1到1(即元素0)。sum_left是3。current_sum=1+3=4>3。所以无法选中右边元素。递归处理右边的元素0的区间。此时,在右边的处理中,sum_left=3>3-current_sum=1. 所以递归处理左边区间,即元素0的区间。这时候,处理区间是1-1。sum_left=3。current_sum+3=4>3,所以递归处理左边的区间,即元素0的区间?或者,当处理元素0的区间时,sum_left=3,current_sum=1+3=4>3,所以无法选中,递归处理左边的区间,即元素0的左边?
这个时候,区间是1-1。调用solve函数:
sum_left是元素0的b=3。current_sum是1,sum_left=3. 1+3=4>3. 所以无法选中。递归处理左边区间,即调用solve(arr, l=1, m=1, current_sum=1, x=3). 此时,进入递归:
处理区间1-1。选pivot是元素0(此时可能索引1的元素是(2,3)?或者可能我之前的例子构造错了?或者可能在这个例子中,原数组被划分后的结构?
可能这个例子需要重新构造。
原数组的元素是元素0:(a=2,b=3),元素1:(a=1,b=1)。初始数组是[(2,3),(1,1)]。
第一次调用solve处理区间0-1。选择pivot为元素1(a=1)。划分后的左边是元素1,sum_left=1。current_sum=0+1=1 <=3。所以处理右边区间,即元素0。sum_left=3. current_sum becomes 1+3=4>3,所以无法选中。递归处理右边区间,即元素0的区间[0]。此时,进入solve函数处理区间0-0,current_sum=1.
在区间0-0中,sum_left=3. current_sum +3=4>3,所以无法选中,递归处理左边区间[0-0]的左边部分?
但是,当处理区间0-0时,调用partition函数,选择pivot,划分后的sum_left=3. current_sum+3=4>3,所以递归处理左边区间0-0的左边部分,即区间0-0的左边部分?这可能进入无限递归?
或者,此时,在递归处理时,当l ==r时,sum_left等于该元素的b。此时,判断current_sum + sum_left <=x。如果成立,返回1,否则递归处理左边区间。但是此时左边区间是同一元素?
这可能有问题。例如,当处理单个元素,且current_sum + sum_left >x,则递归处理该元素的左边区间,即l到m。当m是该元素的位置,此时再次调用solve(arr, l, m, ...),导致无限递归?
哦,这显然是一个错误。比如,当处理区间[0..0],sum_left=3,current_sum=1。current_sum+sum_left=4>3。所以,调用solve(arr, 0, 0, ...),返回的结果是递归处理左边部分,即区间0到0。这会导致无限递归。
这说明,代码中的递归终止条件没有正确处理这种情况。
所以,在递归函数中,当处理区间l ==r时,如果sum_left >x - current_sum,那么无法选中该元素,返回0。否则返回1。
但是当前的代码中,当处理区间[l..r],如果sum_left<=x - current_sum,就选中左边所有元素,否则递归处理左边。但在这种情况下,当左边只有一个元素,而sum_left> x - current_sum时,递归处理左边区间,导致无限递归。
所以,必须修改递归的终止条件。
那问题的根源在于,当处理区间[l..r],并且sum_left> x - current_sum时,是否应该递归处理左边区间[l..m]?此时,左边区间可能包含多个元素,或者一个元素。例如,在单个元素的情况下,m等于l,所以递归处理[l..m]就是处理同样的区间,导致无限循环。
所以,必须修改递归函数,确保在处理单个元素时,能够正确判断是否选中该元素。
可能的解决方案是,在处理递归时,当sum_left> x - current_sum时,需要将左边的元素划分为更小的部分,而不是直接递归整个左边区间。例如,在单个元素的情况下,sum_left是元素的b。如果current_sum + sum_left >x,那么无法选中该元素,返回0。否则返回1。
因此,在递归函数中,必须确保在每次调用时,区间被正确划分,并且不会重复处理同一个区间。
这可能要求,在每次递归调用时,区间的大小至少减少。
例如,在划分后的左边区间是[l..m]。如果m等于原来的l,那么区间的大小减少了吗?例如,当处理区间[l..r],其中l ==r,划分后的左边是[l..m],其中m可能等于 l。此时,递归调用solve(arr, l, m, ...)可能仍然是处理同一个区间,导致无限递归。
这表明,代码中的递归终止条件或者划分逻辑存在缺陷。
这可能是因为,当处理一个区间时,如果sum_left> x - current_sum,那么我们需要在左边的区间中找出可以选中的元素数目。但是,左边的区间可能和原区间一样大,例如,如果划分后的左边区间与原区间相同,就会导致无限递归。
例如,假设所有元素的a相同,那么每次划分后的左边区间是整个原区间,导致每次递归处理同一个区间,从而导致O(n^2)的时间复杂度,或者栈溢出。
这显然是一个问题。因此,需要确保在每次递归调用时,区间的大小严格缩小。
这可能要求在partition函数中,必须确保左边区间和原区间不同。例如,当所有元素的a都相等时,左边区间是原区间,sum_left是总和。如果sum_left> x - current_sum,那么递归处理左边区间,导致无限递归。
这显然需要修正。
那这表明,当前的算法设计存在缺陷,无法处理某些情况,导致无限递归或者错误的结果。
那么,如何解决这个问题?
可能的解决方案是,在递归处理左边区间时,确保该区间的大小比原区间小。这可能需要改变partition函数的逻辑,或者调整递归的处理方式。
比如,在partition函数中,当所有元素的a都等于pivot_a时,划分后的左边区间是整个数组,右边为空。此时,sum_left是总和。如果sum_left> x - current_sum,那么需要递归处理左边区间,但此时左边区间和原区间大小相同,导致无限递归。
这显然是不可接受的。
因此,这个算法必须处理这种情况。
那么,如何在这种情况下避免无限递归?
可能的解决方案是,当sum_left> x - current_sum时,在左边区间中找到最大的子集,使得部分和不超过x - current_sum。但如何在不进行排序的情况下找到这个子集?
在这种情况下,可能需要重新考虑整个算法的设计。
或许,问题出在当所有元素的a相同的情况下,此时它们的排列顺序不影响部分和的总和。在这种情况下,问题简化为找到最大的k,使得前k个元素的b之和不超过x。此时,这个问题等同于找到最大的k,使得部分和不超过x,这在未排序的情况下只能在O(n)时间内解决。
这可能需要另一个处理步骤。例如,当在递归过程中发现sum_left> x - current_sum时,必须遍历左边的元素,并找到最大的k,使得部分和不超过x - current_sum。这在无法划分的情况下(例如所有元素的a相同)必须这样做。
这可能引入O(n)的时间复杂度,但无法保证总体的O(n)时间复杂度。
这可能意味着,当前的算法设计无法处理所有情况,或者需要调整。
那么,回到原问题,是否可以采用另一种方式?
比如,将问题转化为寻找最大的k,使得在排序后的数组中前k+1个元素的和不超过x。由于排序后的数组的a是递增的,所以问题与元素的顺序无关,只与它们的a的顺序有关。因此,可以认为,当元素的a越小的,在部分和中越先被累加。因此,当我们需要尽可能多的元素时,应该选择a较小的元素,因为它们的b之和可能更小。
所以,问题类似于选择尽可能多的元素,其中它们的a必须按升序排列,并且它们的b之和的总和不超过x。这似乎与传统的选择问题不同,因为元素的顺序会影响总和。
或者,这个问题等价于,在所有的元素中,找出一个子集,其中元素的a按升序排列,并且它们的b之和的总和不超过x,且这个子集的大小尽可能大。
这与原问题的要求一致,因为排序后的数组中的元素按照a升序排列,所以部分和即为前k+1个元素的和。而最大的i就是最大的k,使得这部分和不超过x。
因此,问题转化为选择最大的k,使得按a升序排列的前k+1个元素的b之和<=x。
因此,这个问题的解法必须将元素按a升序排列,并计算部分和,直到超过x。但为了在O(n)的时间内完成,需要找到一种方法,类似于快速选择,在未排序的情况下找到这个k。
回到最初的算法,即类似于快速选择的划分,递归处理。
但是,当遇到所有元素的a相同的情况时,该算法可能无法处理,因为划分后的左边区间与原区间大小相同,导致无限递归。
因此,必须在这种情况下进行处理。
可能的解决方案是,当在递归处理中发现sum_left> x - current_sum时,不再递归处理整个左边区间,而是对左边区间的元素进行线性扫描,计算每个元素的部分和,并找到最大的k,使得总和不超过x - current_sum。
这可能破坏整体的O(n)时间复杂度,但在这种情况下,可以将时间摊还到其他情况中。
例如,当划分后的左边区间与原区间大小相同,说明所有元素的a相同。此时,可以线性扫描该区间的元素,计算它们的部分和,直到总和超过x - current_sum。这样,这部分处理的时间是O(m),其中m是区间的大小。这将导致在最坏情况下,时间复杂度为O(n^2)。例如,当所有元素的a相同,且总和超过x时,每次划分后都要线性扫描整个数组,导致O(n)次扫描,每次O(n)时间,总时间O(n^2)。
这显然不符合题目的O(n)时间复杂度要求。
因此,这说明最初的算法设计存在缺陷,无法处理所有情况,尤其是在元素a全相同的极端情况下。
因此,可能需要寻找另一种方法,或者调整算法以处理这种特殊情况。
例如,在每次递归处理时,如果划分后的左边区间与原区间相同,则说明所有元素的a相同。此时,可以直接线性处理这部分元素,找到最大的k,使得他们的总和不超过x - current_sum。这将确保在这种情况下,处理时间为O(n)。
但如何在递归过程中检测这种情况?
例如,在每次调用partition函数后,如果m ==r,即整个区间都被划分为左边,而右边为空,说明所有元素的a <= pivot_a。这可能意味着所有元素的a等于 pivot_a,或者存在比pivot_a大的元素,但在partition过程中可能不会出现这种情况。
或者,这可能无法准确判断。
因此,可能需要另一个方法。
另一个思路是,在递归函数中,当处理一个区间时,如果sum_left> x - current_sum,那么我们无法选中整个左边区间,此时需要找到左边区间中,按a升序排列的前k个元素的和不超过x - current_sum的最大k。由于左边区间的元素的a <= pivot_a,所以在排序后的数组中,左边区间的元素会出现在 pivot_a元素的前面。因此,他们的顺序可能并不完全有序,但他们的a <= pivot_a。因此,在左边区间中,任何元素的a都<= pivot_a,而右边元素的a> pivot_a。因此,当无法选中整个左边区间时,必须递归处理左边区间,以找到其中最大的k。
但在这种情况下,左边区间的元素的a可能都是相同的,或者可能包含更小的a的元素。
因此,回到原来的递归逻辑,当sum_left> x - current_sum时,递归处理左边区间。但是,当左边区间与原区间的大小相同(例如,所有元素的a相同),这将导致无限递归。
要解决这个问题,必须确保在递归处理左边区间时,区间的大小每次都会减少。
可能的解决方法是,在partition函数中,将等于pivot_a的元素随机分配到左边或右边。或者,使用三向划分,将等于pivot_a的元素放在中间。这可能有助于避免在大量重复元素时陷入无限递归。
例如,使用三向划分(类似快速排序中的三向划分),将数组划分为小于pivot_a、等于pivot_a、大于pivot_a的三部分。在这种情况下,当sum_left是小于等于pivot_a的元素的总和,如果sum_left> x - current_sum,则递归处理小于pivot_a的部分,这可能为空。如果等于pivot_a的部分的总和也超过x - current_sum,则处理等于pivot_a的部分。但这可能同样复杂。
这可能是一个可行的解决方案。例如,三向划分:
将数组划分为三部分:a < pivot_a,a == pivot_a, a> pivot_a.
计算左边(a < pivot_a)的总和sum_less,中间(a == pivot_a)的总和sum_eq.
当处理时,首先判断sum_less是否 <= x - current_sum。如果成立,current_sum += sum_less,然后判断sum_eq是否 <=x - current_sum。如果成立,current_sum += sum_eq,返回sum_less+sum_eq的数里,并处理右边。否则,处理中间部分,找到其中可以选中的元素数目,使得总和不超过x - current_sum。否则,处理左边的部分。
这可能更复杂,但可以避免无限递归的情况。
例如,在处理中间部分(a==pivot_a)时,可以按任意顺序选择这些元素,因为它们的a相同,所以在排序后的数组中,它们的顺序不影响总和。因此,在中间部分,我们可以贪心地选择尽可能多的元素,使得它们的总和不超过x - current_sum.
这可能将问题简化为,在中间部分的元素中,按任意顺序选择元素,直到总和超过x - current_sum。因为中间部分的元素的a相同,所以顺序无关,总和的计算可以按任意顺序进行。因此,这部分的处理可以在O(m)时间内完成,其中m是中间部分的元素数目。
这样,在递归处理时,三向划分可以解决所有元素的a相同的问题。
因此,修改算法如下:
1. 在每次递归处理时,将当前区间三划分为小于pivot_a、等于pivot_a、大于pivot_a的三部分。
2. 计算sum_less(a < pivot_a的总和)、sum_eq(a == pivot_a的总和)。
3. 如果current_sum + sum_less <=x,则选中所有a < pivot_a的元素,current_sum += sum_less。然后判断sum_eq是否 <=x - current_sum。如果是,选中所有a==pivot_a的元素,current_sum +=sum_eq,返回数目加上右边递归处理的结果。否则,在a==pivot_a的元素中,按任意顺序选择尽可能多的元素,使得总和不超过x - current_sum。然后返回数目。如果无法选中任何a==pivot_a的元素,则返回a < pivot_a的数目加上右边处理的结果。
这可能能正确处理所有情况,包括元素a全相同的情况。
但这种三向划分的实现可能较为复杂,并且增加算法的时间复杂度。
因此,在时间允许的情况下,可能需要尝试这样的方法。
综上,回到最初的算法,可能无法处理所有情况,比如所有元素的a相同的情况。因此,需要修改递归处理的方式,以避免无限递归。
可能的解决方案是,当递归处理左边区间时,确保该区间的大小比原区间小。这可以通过在partition函数中,确保左边和右边区间都至少有一个元素,或者将pivot元素单独处理。
例如,在partition函数中,将pivot元素单独放在中间位置,然后左边是a < pivot_a,右边是a > pivot_a。中间是等于pivot_a的元素。这可能避免在递归处理时陷入无限循环。
例如,在每次划分时,将数组划分为三个部分:a < pivot_a、a == pivot_a、a> pivot_a。然后,sum_less是左边部分的总和,sum_eq是中间部分的总和。
递归处理时,首先处理左边部分(a < pivot_a),然后中间部分(a == pivot_a),然后右边部分(a > pivot_a)。
在处理当前区间时:
sum_less是左边部分的总和,sum_eq是中间部分的总和。
如果 current_sum + sum_less <=x:
current_sum += sum_less
数目 += 左边部分的元素个数
if current_sum + sum_eq <=x:
current_sum += sum_eq
数目 += 中间部分的元素个数
数目 += 递归处理右边部分的结果
else:
// 在中间部分中选择尽可能多的元素,使得总和不超过x - current_sum
// 因为中间部分的元素的a相同,顺序无关,可以任意排列
数目 += 在中间部分中按顺序累加,直到总和超过x - current_sum,取前k个
return数目
else:
数目 += 递归处理左边部分的结果
return数目
这可能是一个可行的解决方案。
这样,在处理中间部分的元素时,只需累加它们的b值,直到总和超过x - current_sum,即可得到最大的k。
这样,无论中间部分的元素顺序如何,只要它们的a相同,就可以任意顺序累加,得到最大的k。
这样,在中间部分中,可以线性遍历元素,累加它们的b,直到总和超过x - current_sum。
这将确保在处理中间部分时,时间复杂度为O(m),其中m是中间部分的元素数目。
这样,整个算法的时间复杂度可能仍然是O(n),因为每个元素至多被处理常数次。
这可能解决元素a全相同的问题。
因此,这样的算法可能更鲁棒,能够处理所有情况。
综上,为了实现这样的算法,需要:
1. 三向划分,将数组划分为左边(a < pivot)、中间(a == pivot)、右边(a > pivot)三部分。
2. 在递归处理时,先处理左边,然后中间,然后右边。
3. 中间部分的处理需要线性遍历,累加元素,直到总和超过x - current_sum。
这可能比较复杂,但可以确保正确性。
因此,现在需要重新设计partition函数,以支持三向划分,并计算sum_less和sum_eq。
三向划分的实现可以采用类似快速排序的三向划分方法。
例如:
void partition(vector<pair<double, double>>& arr, int l, int r, int& less_end, int& great_start, double& sum_less, double& sum_eq) {
// 随机选择pivot索引
int pivot_idx = l + rand() % (r - l + 1);
double pivot_a = arr[pivot_idx].first;
sum_less = 0;
sum_eq = 0;
int i = l; // 当前处理的位置
int less = l; // 指向a < pivot的元素的末尾+1
int great = r; // 指向a > pivot的元素的开头-1
while (i <= great) {
if (arr[i].first < pivot_a) {
swap(arr[i], arr[less]);
sum_less += arr[less].second;
less++;
i++;
} else if (arr[i].first > pivot_a) {
swap(arr[i], arr[great]);
great--;
} else { // equal
sum_eq += arr[i].second;
i++;
}
}
less_end = less -1;
great_start = great +1;
}
这样,划分后的数组结构是:
- [l..less_end]:a < pivot_a
- [less_end+1..great_start-1]: a == pivot_a
- [great_start..r]: a > pivot_a
sum_less是a < pivot_a的元素的总和,sum_eq是a == pivot_a的元素的总和。
这样,在递归处理时,可以处理这三个部分。
递归函数的逻辑如下:
int solve(vector<pair<double, double>>& arr, int l, int r, double& current_sum, double x) {
if (l > r) return 0;
if (l == r) {
if (current_sum + arr[l].second <= x) {
current_sum += arr[l].second;
return 1;
} else {
return 0;
}
}
int less_end, great_start;
double sum_less, sum_eq;
partition(arr, l, r, less_end, great_start, sum_less, sum_eq);
int count = 0;
// 处理左边部分(a < pivot)
if (sum_less > 0) {
if (current_sum + sum_less <= x) {
current_sum += sum_less;
count += (less_end - l + 1);
// 处理中间部分(a == pivot)
if (current_sum + sum_eq <= x) {
current_sum += sum_eq;
count += (great_start - less_end - 1);
// 处理右边部分(a > pivot)
count += solve(arr, great_start, r, current_sum, x);
return count;
} else {
// 在中间部分中找到最大的k
double temp_sum = 0;
for (int i = less_end + 1; i < great_start; i++) {
if (current_sum + temp_sum + arr[i].second > x) {
current_sum += temp_sum;
count += (i - (less_end + 1));
return count;
}
temp_sum += arr[i].second;
}
current_sum += temp_sum;
count += (great_start - less_end -1);
return count;
}
} else {
// 处理左边部分
count += solve(arr, l, less_end, current_sum, x);
return count;
}
} else {
// 没有左边部分,直接处理中间部分
if (current_sum + sum_eq <= x) {
current_sum += sum_eq;
count += (great_start - l);
count += solve(arr, great_start, r, current_sum, x);
return count;
} else {
double temp_sum = 0;
for (int i = l; i < great_start; i++) {
if (current_sum + temp_sum + arr[i].second > x) {
current_sum += temp_sum;
count += (i - l);
return count;
}
temp_sum += arr[i].second;
}
current_sum += temp_sum;
count += (great_start - l);
return count;
}
}
}
这可能比较复杂,但可以处理所有情况。
例如,当所有元素的a相同时,sum_less为0,sum_eq为所有元素的总和。此时,current_sum + sum_eq是否 <=x?如果是,返回所有元素数目,否则在中间部分遍历,累加直到超过x。
这将确保在这种情况下,线性处理中间部分,避免无限递归。
这样,递归函数的时间复杂度为O(n),因为每个元素至多被处理一次:在划分时,或者在中间部分的线性遍历时。
综上,这样的算法可能正确,并且时间复杂度为O(n)。
现在,回到最初的问题,需要将原数组按a升序排列,并计算部分和。所以,三向划分后的处理顺序是正确的,因为左边部分的a < pivot_a,中间等于,右边大于。处理顺序是左边、中间、右边,这符合a升序排列的顺序。
综上,这样的算法可能正确,并且能够在O(n)的平均时间复杂度内解决问题。
Answer
========
为了解决这个问题,我们需要找到最大的索引i,使得在按a升序排列后的数组的前i+1个元素的b值之和不超过给定的x。为了在O(n)的平均时间复杂度内完成,我们采用类似于快速选择算法的分治法,结合三向划分来处理可能的重复元素。
### 方法思路
1. **随机打乱数组**:避免最坏情况下的时间复杂度。
2. **三向划分**:将数组划分为三个部分:小于pivot、等于pivot和大于pivot的元素,以便处理重复元素。
3. **递归处理**:根据当前部分的和与x的关系,决定递归处理左边、中间或右边的子数组。
4. **线性扫描中间部分**:当中间部分的和超过剩余x值时,线性扫描以找到最大的可累加元素数目。
### 解决代码
```cpp
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
void random_shuffle(vector<pair<double, double>>& arr) {
srand(time(nullptr));
for (int i = arr.size() - 1; i > 0; --i) {
int j = rand() % (i + 1);
swap(arr[i], arr[j]);
}
}
void partition(vector<pair<double, double>>& arr, int l, int r, int& less_end, int& great_start, double& sum_less, double& sum_eq) {
int pivot_idx = l + rand() % (r - l + 1);
double pivot_a = arr[pivot_idx].first;
sum_less = 0;
sum_eq = 0;
int less = l;
int great = r;
int i = l;
while (i <= great) {
if (arr[i].first < pivot_a) {
swap(arr[i], arr[less]);
sum_less += arr[less].second;
less++;
i++;
} else if (arr[i].first > pivot_a) {
swap(arr[i], arr[great]);
great--;
} else {
sum_eq += arr[i].second;
i++;
}
}
less_end = less - 1;
great_start = great + 1;
}
int solve(vector<pair<double, double>>& arr, int l, int r, double& current_sum, double x) {
if (l > r) return 0;
if (l == r) {
if (current_sum + arr[l].second <= x) {
current_sum += arr[l].second;
return 1;
} else {
return 0;
}
}
int less_end, great_start;
double sum_less, sum_eq;
partition(arr, l, r, less_end, great_start, sum_less, sum_eq);
int count = 0;
// Process less than partition
if (l <= less_end) {
if (current_sum + sum_less <= x) {
current_sum += sum_less;
count += (less_end - l + 1);
// Process equal partition
if (current_sum + sum_eq <= x) {
current_sum += sum_eq;
count += (great_start - less_end - 1);
// Process greater partition
count += solve(arr, great_start, r, current_sum, x);
return count;
} else {
// Need to pick some in equal partition
double temp_sum = 0;
for (int i = less_end + 1; i < great_start; ++i) {
if (current_sum + temp_sum + arr[i].second > x) {
current_sum += temp_sum;
count += (i - (less_end + 1));
return count;
}
temp_sum += arr[i].second;
}
current_sum += temp_sum;
count += (great_start - less_end - 1);
return count;
}
} else {
// Recurse on less than partition
count += solve(arr, l, less_end, current_sum, x);
return count;
}
} else {
// No less than partition, process equal partition
if (current_sum + sum_eq <= x) {
current_sum += sum_eq;
count += (great_start - l);
// Process greater partition
count += solve(arr, great_start, r, current_sum, x);
return count;
} else {
double temp_sum = 0;
for (int i = l; i < great_start; ++i) {
if (current_sum + temp_sum + arr[i].second > x) {
current_sum += temp_sum;
count += (i - l);
return count;
}
temp_sum += arr[i].second;
}
current_sum += temp_sum;
count += (great_start - l);
return count;
}
}
}
int main() {
int n;
double x;
cin >> n >> x;
vector<pair<double, double>> arr(n);
for (int i = 0; i < n; ++i) {
cin >> arr[i].first >> arr[i].second;
}
random_shuffle(arr);
double current_sum = 0;
int k = solve(arr, 0, n - 1, current_sum, x);
if (k == 0) {
cout << -1 << endl;
} else {
cout << k - 1 << endl;
}
return 0;
}
```
### 代码解释
1. **随机打乱数组**:使用Fisher-Yates算法打乱数组以避免最坏情况。
2. **三向划分**:将数组划分为小于、等于和大于pivot的三个部分,并计算各部分的和。
3. **递归处理**:根据当前部分的和与x的关系,决定递归处理左边、中间或右边的子数组。
4. **线性扫描中间部分**:当中间部分的和超过剩余x值时,线性扫描以找到最大的可累加元素数目。
该方法通过分治法和三向划分确保平均时间复杂度为O(n),能够高效地解决问题。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment