基于计数排序求中位数问题的O(1)算法(Fraudulent Activity Notifications问题)
2017-02-10 12:53
375 查看
原问题(Fraudulent Activity Notifications)链接:https://www.hackerrank.com/challenges/fraudulent-activity-notifications
Fraudulent Activity Notifications问题摘要:
HackerLand National Bank has a simple policy for warning clients about possible fraudulent account activity. If the amount spent by a client on a particular day is greater than or equal to 2×the client’s median spending for the last d days, they send the client a notification about potential fraud. The bank doesn’t send the client any notifications until they have at least d prior days of transaction data.
Given the value of d and a client’s total daily expenditures for a period of n days, find and print the number of times the client will receive a notification over all n days.
这里使用了计数排序(Counting Sort)的思想,用C++写了一个类CountingHeap(因为这种数据结构有点像一个堆,故命名为CountingHeap,但并不是STL的“堆”数据结构)用于解决该问题。就CountingHeap而言,其更新中位数的算法时间复杂度为O(1),空间复杂度为O(range),之前写的一个版本基于插入排序的算法时间复杂度为O(range),空间复杂度O(range),相比之下速度提高了不少。使用Hackerrank上此问题的Test Case #1测试,原先算法需时5.768秒,当前算法需0.173秒。
CountingHeap中使用curSize指示当前元素总数,size[]数组保存各取值出现次数,形成一个[0,range)的“计数堆”。可以想象成编号为0~range-1的空心柱子,每添加一个数就往那个数对应的柱子里面放一个球,size[]数组保存的就是各空心柱子的球数。例如,初始为空,每个柱子都是空的,先添加一个元素2,就往编号为2的柱子里面放一个球,此时size[2]++,从0变为1。
CountingHeap中使用midA和midB指示两个中位数,并且保持midA<=midB。为什么要使用两个变量来表示中位数呢?因为若当前元素为偶数个,则中位数应该取中间两个数的平均数,而这样可能会带来分数,而使用double类型则不能作为索引访问size[]数组,故取两个变量midA,midB分别表示左侧的中位数和右侧的中位数,则两者之和为中位数的2倍。元素总数为奇数个时,midA=midB;偶数个时,midA在左,midB在右,此时midA的下一个元素就是midB。由于可能存在相同的元素,故使用结构体Mid,number成员表示指示的数字,No成员表示所指数字在同值元素中的索引,如第1个出现则No=0,第2个出现则No=1,以此类推。
CountingHeap中位数的实时更新通过移动这两个元素来实现。这里定义了两个private方法moveLeft(Mid &mid)和moveRight(Mid &mid)。以moveLeft为例,将mid左移一位,若当前所指元素已经是同值元素中最左边的(No=0),则先将number减一,寻找左边紧接着的元素;若当前所指元素不是同值元素中最左边的(No>0),则只需将No减一即可。moveRight同理。
接下来需要做的就是实现public中的addElement(int x)方法和deleteElement(int x)方法,这两个方法均借助moveLeft和moveRight实现。这两个方法总体类似,都需要考虑curSize的奇偶,根据x与midA,midB的相对位置进行移动。相对位置分三种,x < midA,midA < x < midB 与 midB < x,其中中间的情况要根据curSize的奇偶来定。因为当curSize为奇数时,添加或删除元素前的midA和midB所指示的并不是同一个元素,而当curSize为偶数时,添加或删除元素前的midA和midB必定是相等的。这两种情况要分别考虑,奇数的时候中间的情况为midA <= x < midB,而偶数的时候中间的情况为midA <= x <= midB。
添加、删除操作midA,midB移动对应表如下:
注:A,B分别指midA,midB.
注(1):当curSize为奇数时为A <= x < B,偶数时为A <= x <= B。相应地,奇数时下一栏为B <= x,偶数时下一栏为B < x.
注(2):当B所指元素被删掉时B需要右移,否则B不需要移动
源代码如下:(注释部分用于输出调试信息)
Fraudulent Activity Notifications问题摘要:
HackerLand National Bank has a simple policy for warning clients about possible fraudulent account activity. If the amount spent by a client on a particular day is greater than or equal to 2×the client’s median spending for the last d days, they send the client a notification about potential fraud. The bank doesn’t send the client any notifications until they have at least d prior days of transaction data.
Given the value of d and a client’s total daily expenditures for a period of n days, find and print the number of times the client will receive a notification over all n days.
这里使用了计数排序(Counting Sort)的思想,用C++写了一个类CountingHeap(因为这种数据结构有点像一个堆,故命名为CountingHeap,但并不是STL的“堆”数据结构)用于解决该问题。就CountingHeap而言,其更新中位数的算法时间复杂度为O(1),空间复杂度为O(range),之前写的一个版本基于插入排序的算法时间复杂度为O(range),空间复杂度O(range),相比之下速度提高了不少。使用Hackerrank上此问题的Test Case #1测试,原先算法需时5.768秒,当前算法需0.173秒。
CountingHeap中使用curSize指示当前元素总数,size[]数组保存各取值出现次数,形成一个[0,range)的“计数堆”。可以想象成编号为0~range-1的空心柱子,每添加一个数就往那个数对应的柱子里面放一个球,size[]数组保存的就是各空心柱子的球数。例如,初始为空,每个柱子都是空的,先添加一个元素2,就往编号为2的柱子里面放一个球,此时size[2]++,从0变为1。
CountingHeap中使用midA和midB指示两个中位数,并且保持midA<=midB。为什么要使用两个变量来表示中位数呢?因为若当前元素为偶数个,则中位数应该取中间两个数的平均数,而这样可能会带来分数,而使用double类型则不能作为索引访问size[]数组,故取两个变量midA,midB分别表示左侧的中位数和右侧的中位数,则两者之和为中位数的2倍。元素总数为奇数个时,midA=midB;偶数个时,midA在左,midB在右,此时midA的下一个元素就是midB。由于可能存在相同的元素,故使用结构体Mid,number成员表示指示的数字,No成员表示所指数字在同值元素中的索引,如第1个出现则No=0,第2个出现则No=1,以此类推。
CountingHeap中位数的实时更新通过移动这两个元素来实现。这里定义了两个private方法moveLeft(Mid &mid)和moveRight(Mid &mid)。以moveLeft为例,将mid左移一位,若当前所指元素已经是同值元素中最左边的(No=0),则先将number减一,寻找左边紧接着的元素;若当前所指元素不是同值元素中最左边的(No>0),则只需将No减一即可。moveRight同理。
接下来需要做的就是实现public中的addElement(int x)方法和deleteElement(int x)方法,这两个方法均借助moveLeft和moveRight实现。这两个方法总体类似,都需要考虑curSize的奇偶,根据x与midA,midB的相对位置进行移动。相对位置分三种,x < midA,midA < x < midB 与 midB < x,其中中间的情况要根据curSize的奇偶来定。因为当curSize为奇数时,添加或删除元素前的midA和midB所指示的并不是同一个元素,而当curSize为偶数时,添加或删除元素前的midA和midB必定是相等的。这两种情况要分别考虑,奇数的时候中间的情况为midA <= x < midB,而偶数的时候中间的情况为midA <= x <= midB。
添加、删除操作midA,midB移动对应表如下:
操作 | 添加 | 删除 | ||
curSize | 奇数 | 偶数 | 奇数 | 偶数 |
x < A | <-B | <-A | A-> | B-> |
A < x < B(1) | A-> <-B | B-> | A-> | <-A (B->)(2) |
B < x | A-> | B-> | <-B | <-A |
注(1):当curSize为奇数时为A <= x < B,偶数时为A <= x <= B。相应地,奇数时下一栏为B <= x,偶数时下一栏为B < x.
注(2):当B所指元素被删掉时B需要右移,否则B不需要移动
源代码如下:(注释部分用于输出调试信息)
#include <iostream> #include <fstream> using namespace std; class CountingHeap { public: CountingHeap(){} CountingHeap(int inRange): midA(-1,0), midB(inRange,0) { range = inRange; size = new int [range]; for(int i=0;i<range;i++) size[i] = 0; curSize = 0; } ~CountingHeap(){ delete []size; } int getCurSize(){ return curSize; } int getMidA(){ return midA.number; } int getNoA(){ return midA.No; } int getMidB(){ return midB.number; } int getNoB(){ return midB.No; } int get2Mid(){ return midA.number+midB.number; } void printSortedArray() { cout << " >" << curSize << ": "; for(int i=0;i<range;i++) for(int j=0;j<size[i];j++) cout << i << " "; 4000 cout << endl; } void addElement(int x) { curSize++; size[x]++; if(curSize==1) { midA.number = midB.number = x; midA.No = midB.No = 0; } else if(curSize%2) { if(x<midA.number) moveLeft(midB); else if(midA.number<=x && x<midB.number) { moveLeft(midB); moveRight(midA); } else moveRight(midA); } else { if(x<midA.number) moveLeft(midA); else if(midA.number<=x && x<=midB.number) moveRight(midB); else moveRight(midB); } } void deleteElement(int x) { curSize--; size[x]--; if(curSize%2) { if(x<midA.number) moveRight(midA); else if(midA.number<=x && x<midB.number) moveRight(midA); else moveLeft(midB); } else { if(x<midA.number) moveRight(midB); else if(midA.number<=x && x<=midB.number) { moveLeft(midA); if(size[midB.number]<=midB.No) moveRight(midB); } else moveLeft(midA); } } private: int *size; int range; int curSize; struct Mid { int number; int No; Mid(){} Mid(int inNumber, int inNo){ number = inNumber; No = inNo; } }; Mid midA; Mid midB; void moveLeft(Mid &mid) { //cout << mid.number << "(" << mid.No << "):left"; if(mid.No==0) { mid.number--; while(mid.number>=0&&size[mid.number]==0) mid.number--; mid.No = size[mid.number]-1; } else mid.No--; //cout << "->" << mid.number << "(" << mid.No << ")" << endl; } void moveRight(Mid &mid) { //cout << mid.number << "(" << mid.No << "):right"; if(mid.No>=size[mid.number]-1) { mid.number++; while(mid.number<range&&size[mid.number]==0) mid.number++; mid.No = 0; } else mid.No++; //cout << "->" << mid.number << "(" << mid.No << ")" << endl; } }; int main() { /* ifstream input("E:\\input01.txt"); cin.rdbuf(input.rdbuf()); ofstream output("E:\\output01.txt"); cout.rdbuf(output.rdbuf()); */ int n,d; cin >> n >> d; int *ar = new int ; for(int i=0;i<n;i++) cin >> ar[i]; int cnt = 0; CountingHeap hp(201); for(int i=0;i<n;i++) { if(hp.getCurSize()<d) { hp.addElement(ar[i]); //cout << i << " " << hp.getMidA() << " " << hp.getMidB() << endl; } else { //cout << "(" << ar[i] << "," << hp.get2Mid() << ")" << endl; if(ar[i]>=hp.get2Mid()) cnt++; hp.addElement(ar[i]); hp.deleteElement(ar[i-d]); } //hp.printSortedArray(); //cout << "midA=" << hp.getMidA() << "(" << hp.getNoA() << ") midB=" << hp.getMidB() << "(" << hp.getNoB() << ")" << endl; } cout << cnt << endl; return 0; }
相关文章推荐
- 蛙蛙推荐:基于标记窗的网页正文提取算法的一些细节问题
- 2n个数的中位数问题_python_算法与数据结构
- 非基于比较的排序算法之一:计数排序
- 求两个等长有序数组中位数算法问题
- 基于startActivityForResult方法处理两个Activity之间数据传递问题
- 基于数据库的公交换乘算法(一点思路一点问题)
- 基于C++的农夫过河问题算法设计与实现方法
- (基于Java)算法之动态规划——0-1背包问题
- (基于Java)算法之动态规划——矩阵连乘问题
- 打印质数的算法----基于算法复杂度的考虑,很多问题并不那么简单
- 算法问题基于蚁群算法求解求解TSP问题(JAVA)
- (基于Java)算法之贪心算法——活动安排问题
- 算法实践篇-基于计数排序的基数排序
- [导入]蛙蛙推荐:基于标记窗的网页正文提取算法的一些细节问题
- 基于opencv的神经网络算法实现两类分类问题的可视化演示
- Java基于循环递归回溯实现八皇后问题算法示例