引子
最近自己的獨立游戲上線了,算是了卻了一樁心願。接下來還是想找一份工作繼續干,創業的事有緣再說。
找工作之前,總是要瀏覽一些實戰題目,熱熱身嘛。通過搜索引擎,搜到了今日頭條的一道面試題。
題目
P為給定的二維平面整數點集。定義 P 中某點x,如果x滿足 P 中任意點都不在 x 的右上方區域內(橫縱坐標都大於x),則稱其為“最大的”。求出所有“最大的”點的集合。(所有點的橫坐標和縱坐標都不重復, 坐標軸范圍在[0, 1e9) 內)
如下圖:實心點為滿足條件的點的集合。請實現代碼找到集合 P 中的所有 ”最大“ 點的集合並輸出。

輸入描述:
第一行輸入點集的個數 N, 接下來 N 行,每行兩個數字代表點的 X 軸和 Y 軸。
輸出描述:
輸出“最大的” 點集合, 按照 X 軸從小到大的方式輸出,每行兩個數字分別代表點的 X 軸和 Y軸。
輸入例子1:
5 1 2 5 3 4 6 7 5 9 0
輸出例子1:
4 6 7 5 9 0
思路
1.暴力搜索法
先取一點,然后和其他所有點比較,看看是否有點在其右上方,沒有則證明該點是“最大點”。重復檢測所有的點。顯而易見,算法的復雜度為O(n2)
2.變治法(預排序)
由“最大點”的性質可知,對於每一個“最大點”,若存在其他點的y值大於該點y值,那么其他點x值必然小於該點的x值。
換言之,當某一點確定它的x值高於所有y值大於它的點的x值,那么該點就是“最大點” 。網上給出的答案基本上都是這個套路。
對於y有序的點集,只需要O(n)即可輸出“最大點”點集。一般基於比較的排序算法時間復雜度O(nlogn)。那么,顯而易見,算法整體復雜度為O(nlogn)。
3.減治法+變治法(過濾+預排序)
過濾很簡單,就是在集合中找出一個比較好的點,然后過濾掉其左下角的所有點。然后再采用方法2對過濾后的點集求解。
那么這個集合中比較好的點,怎么找,或者說哪個點是比較好的點。顯而易見,越靠近點集右上角的點,左下角的面積就越大,越可以過濾更多的點,故越好。
兒時學過,兩個數的和一定,那么兩數差越小,乘積越大。簡單設計,該點x和y的和減去x和y差的絕對值越大,該點越好。
4.空間分割(優化的四叉樹)
因為之前對四叉樹有一定的了解,所以對於這個問題也想到能不能有四叉樹去處理。如果有同學熟悉KD樹思路大致是一樣。 為了優化將點集插入到四叉樹的時間,筆者使用預先開辟數組來表示四叉樹。對於500000個點,大概所需時間如下:
build tree cost: 167ms 105233923 999996852 398502327 999994996 837309014 999994263 899779160 999993102 980746971 999976098 990941053 999881685 991773349 999539486 996437667 999536388 999934209 999456481 999948454 989946068 999951793 933115039 999993165 920862637 999996248 471725091 search tree cost: 106ms
假設點集數量為n,構建n次矩形查詢,第i次查詢范圍為以第i個點為左下角以數據最大范圍為右上角的矩形,若該點右上方沒有點那么該點為“最大點”。若該點為“最大點”,我們還可以以它為右上角,以0點位左下角構建范圍查詢,過濾哪些不需要查詢的點。通過對比可知,查詢時間可以接受,但構建時間確實長了一些。可能導致該方法還沒有直接預排序好。總體時間復雜度應該也是O(nlogn)級別,大部分時間都用於構建了,構建時涉及到的內存操作比較多必然消耗更多時間。本來沒有使用預排序就可以獲取結果,我們的輸出結果可以匹配輸入順序,牛客網的答案要求以x從小到大排序,故結果還要重新排序。。。如果結果集非常大,對結果集排序會消耗大量時間。很可惜,四叉樹無法通過測試,一是C#的輸入輸出會占用大量時間,二是我們還需要對結果集重新排序。但實驗證明了對於隨機500000點,使用四叉樹可以和預排序有着差不多的時間復雜度。最后,還有交代一個重要問題:
int x=999999991; float y = 1000000000f; float z = x/y;
請問z值為何?答案是1.0f,因為float的精度不足,無法表示0.999999991,所以變成了1.0f,這個問題一度使我的四叉樹以為小概率出現異常。找了半天才揪出來。
附上代碼,備忘。
using System.IO; using System; using System.Collections.Generic; using System.Diagnostics; class Program { private static int maxData = 1000000000; private static List<MyPoint> dataPoints = new List<MyPoint>(); private static Random rand = new Random(); static void Main() { float time = 0; Stopwatch watch = new Stopwatch(); watch.Start(); Quadtree quad = new Quadtree(5, new Rectangle(0, 0, maxData, maxData)); int count = int.Parse(Console.ReadLine()); for (int i = 0; i < count; i++) { MyPoint temp; //string[] inputs = Console.ReadLine().Split(new char[] { ' ' }); //temp.x = Convert.ToInt32(inputs[0]); //temp.y = Convert.ToInt32(inputs[1]); temp.x = rand.Next(maxData); temp.y = rand.Next(maxData); dataPoints.Add(temp); quad.Insert(temp); } time = watch.ElapsedMilliseconds - time; Console.WriteLine("build tree cost: " + time + "ms"); List<MyPoint> result = new List<MyPoint>(); Rectangle rect; rect.width = rect.height = maxData + 1; for (int i = 0; i < count; i++) { rect.x = dataPoints[i].x; rect.y = dataPoints[i].y; if (quad.retrieve(rect)) { continue; } result.Add(dataPoints[i]); } //要以x軸y從小到大輸出,所以結果集需要排序 result.Sort(); for(int i=0;i< result.Count; i++) { Console.WriteLine( result[i]); } time = watch.ElapsedMilliseconds - time; Console.WriteLine("search tree cost: " + time + "ms"); watch.Stop(); } } public class Quadtree { private class QuadtreeData { public int maxLevel; public double maxWidth; public double maxHeight; public Quadtree[] allNodes; public QuadtreeData(int maxLevel,float maxWidth,float maxHeight) { this.maxLevel = maxLevel; this.maxWidth = maxWidth; this.maxHeight = maxHeight; int maxNodes = 0; for (int i = 0; i <= maxLevel; i++) { maxNodes += (int)Math.Pow(4, i); } allNodes = new Quadtree[maxNodes]; } } private int level; private int parent; private int count; private List<MyPoint> points; private Rectangle bounds; private Quadtree[] nodes; private QuadtreeData data; public Quadtree(int maxLevel,Rectangle bounds) { data = new QuadtreeData(maxLevel,bounds.width,bounds.height); this.bounds = bounds; level = 0; count = 0; parent = -1; nodes = new Quadtree[4]; Init(); } private void Init() { data.allNodes[0] = this; for (int i = 0; i < data.allNodes.Length; i++) { if (data.allNodes[i].level >= data.maxLevel) break; InitChildrenNew(i); } } private void InitChildrenNew(int parentIndex) { Rectangle bounds = data.allNodes[parentIndex].bounds; float subWidth = (bounds.getWidth() / 2); float subHeight = (bounds.getHeight() / 2); float x = bounds.getX(); float y = bounds.getY(); int nextLevel = data.allNodes[parentIndex].level + 1; byte[,] offset =new byte[,]{{0,0},{1,0},{0,1},{1,1}}; for (int i = 0; i < 4; i++) { Rectangle rect = new Rectangle(x,y,subWidth,subHeight); rect.x += offset[i,0]*subWidth; rect.y += offset[i,1]*subHeight; int childIndex = GetPointIndexByLevel(rect.getCenter(), nextLevel); if (childIndex < data.allNodes.Length) { data.allNodes[childIndex] = new Quadtree(nextLevel, rect, data); data.allNodes[childIndex].parent = parentIndex; data.allNodes[parentIndex].nodes[i] = data.allNodes[childIndex]; //Console.WriteLine("p:"+parentIndex+",c:"+childIndex+",size:"+ rect.width); } } } private Quadtree(int pLevel, Rectangle pBounds , QuadtreeData pData) { level = pLevel; bounds = pBounds; nodes = new Quadtree[4]; count = 0; data = pData; } public int GetPointIndexByLevel(MyPoint point, int targetLevel) { int[] indexByLevel={0,1,5,21,85,341,1365,5461,21845}; int startIndex =indexByLevel[targetLevel] ; int cc = (int)Math.Pow(2, targetLevel); //if(point.x >= data.maxWidth || point.y >=data.maxHeight) //{ // Console.WriteLine("error point:"+point); // Console.WriteLine("data:"+data.maxWidth+","+data.maxHeight); //} int locationX = (int)(point.x / data.maxWidth * cc); int locationY = (int)(point.y / data.maxHeight * cc); int idx = startIndex + locationY * cc + locationX; return idx; } /* * Insert the object into the quadtree. If the node * exceeds the capacity, it will split and add all * objects to their corresponding nodes. */ public void Insert(MyPoint point) { int idx = GetPointIndexByLevel(point, data.maxLevel); var nodeToAdd = data.allNodes[idx]; if (nodeToAdd != null) { if (nodeToAdd.points == null) nodeToAdd.points = new List<MyPoint>(); nodeToAdd.points.Add(point); nodeToAdd.AddCount(); } } private void AddCount() { if(parent >=0 ) { var nodeParent = data.allNodes[parent]; nodeParent.AddCount(); } count++; } /* * Return all objects that could collide with the given object */ public bool retrieve(Rectangle pRect) { if(count > 0 && pRect.Contains(bounds)) { return true; } if(count > 0 && bounds.Intersects(pRect)) { if (points != null) { for (int i = 0; i < points.Count; i++) { if (pRect.Contains(points[i])) { return true; } } } else if (level < data.maxLevel) { if (nodes[3] != null && nodes[3].retrieve(pRect)) return true; if (nodes[2] != null && nodes[2].retrieve(pRect)) return true; if (nodes[1] != null && nodes[1].retrieve(pRect)) return true; if (nodes[0] != null && nodes[0].retrieve(pRect)) return true; } } return false; } } public struct MyPoint : IComparable<MyPoint> { public int x; public int y; public MyPoint(int x = 0, int y = 0) { this.x = x; this.y = y; } public override string ToString() { return x + " " + y; } public int CompareTo(MyPoint other) { if(x == other.x) return 0; else if(x > other.x) return 1; else if( x < other.x) return -1; return -1; } } public struct Rectangle { public float x; public float y; public float height; public float width; public Rectangle(float x = 0, float y = 0, float width = 0, float height = 0) { this.x = x; this.y = y; this.width = width; this.height = height; } public float getX() { return x; } public float getY() { return y; } public float getHeight() { return height; } public float getWidth() { return width; } public MyPoint getCenter() { return new MyPoint((int)(x + width / 2), (int)(y + height / 2)); } public bool Intersects(Rectangle Rect) { return (!(y > Rect.y + Rect.height || y + height < Rect.y || x + width < Rect.x || x > Rect.x + Rect.width)); } public bool Contains(MyPoint point) { return (x < point.x && x + width >= point.x && y < point.y && y + height >= point.y); } public bool Contains(Rectangle other) { return Contains(new MyPoint((int)other.x,(int)other.y)) && Contains(new MyPoint((int)(other.x+other.width),(int)(other.y+other.height))); } public override string ToString() { return "Rect:" + x + "," + y + "," + width; } }
過濾與直接預排序對比實現
#include<iostream> #include<algorithm> #include<vector> #include <cstdlib> #include <ctime> using namespace std; struct point{ //定義結構體 int x,y; }; bool cmp(point a,point b){ //自定義排序方法 return a.y==b.y?a.x>b.x:a.y<b.y; //y升序,x降序 } int main(){ clock_t start,finish; double totaltime; std::srand(std::time(nullptr)); // use current time as seed for random generator int count; cout<<"輸入點的個數和點:" ; cin>>count;
cout<<"輸入總點數為:"<<count<<endl; vector<point> p; //容器用來裝平面上的點 for(int i=0;i<count;i++){ point temp; temp.x = std::rand()% 100000000; temp.y = std::rand()% 100000000; p.push_back(temp); //為了方便對比性能,我們隨機插入大量點 }
cout<<"------------------過濾后再使用預排序:------------------------------"<<endl; start = clock(); vector<point> filter;//定義過濾容器 vector<point> res; //定義結果容器 int curMaxRank = 0; int curMaxIndex = 0; for(int i=0;i<count;i++){ int temp =p[i].x+p[i].y-std::abs(p[i].x-p[i].y); if(temp > curMaxRank) { curMaxRank = temp; curMaxIndex = i; } } for(int i=0;i<count;i++) { if(p[i].x >= p[curMaxIndex].x || p[i].y>= p[curMaxIndex].y) { filter.push_back(p[i]); } } sort(filter.begin(),filter.end(),cmp); res.push_back(filter[filter.size()-1]); //左上角的那個點,一定符合條件 int maxx=filter[filter.size()-1].x; for(int i=filter.size()-2;i>=0;i--){ //y從大到小,若i點x值大於所有比其y值大的點的x值,那么i點為“最大點”。 if(filter[i].x>maxx){ res.push_back(filter[i]); maxx=filter[i].x; } } finish = clock(); cout<<"過濾后點數量:"<<filter.size()<<endl; cout<<"符合條件的點數量:"<<res.size()<<endl; for(int i=0;i<res.size();i++){ printf("%d %d\n", res[i].x, res[i].y); } totaltime=(double)(finish-start)/CLOCKS_PER_SEC; cout<<"\n此程序的運行時間為"<<totaltime<<"秒!"<<endl; cout<<"------------------直接使用預排序:------------------------------"<<endl; start = clock(); sort(p.begin(),p.end(),cmp); res.clear(); res.push_back(p[p.size()-1]); //左上角的那個點,一定符合條件 int maxX=p[p.size()-1].x; for(int i=p.size()-2;i>=0;i--){ //y從大到小,若i點x值大於所有比其y值大的點的x值,那么i點為“最大點”。 if(p[i].x>maxX){ res.push_back(p[i]); maxX=p[i].x; } } finish = clock(); cout<<"符合條件的點數量:"<<res.size()<<endl; for(int i=0;i<res.size();i++){ printf("%d %d\n", res[i].x, res[i].y); } totaltime=(double)(finish-start)/CLOCKS_PER_SEC; cout<<"\n此程序的運行時間為"<<totaltime<<"秒!"<<endl; return 0; }
輸入點的個數和點:......輸入總點數為:500000 ------------------過濾后再使用預排序:------------------------------ 過濾后點數量:648 符合條件的點數量:16 15480205 99999697 17427518 99999676 78059606 99999351 80881235 99998746 91608165 99997683 95825638 99996289 99690315 99993155 99874266 99991089 99884382 99978546 99908259 99961095 99942330 99858670 99963997 99157830 99975627 97385053 99996564 95654979 99998236 95378376 99999527 66461920 此程序的運行時間為0.013037秒! ------------------直接使用預排序:------------------------------ 符合條件的點數量:16 15480205 99999697 17427518 99999676 78059606 99999351 80881235 99998746 91608165 99997683 95825638 99996289 99690315 99993155 99874266 99991089 99884382 99978546 99908259 99961095 99942330 99858670 99963997 99157830 99975627 97385053 99996564 95654979 99998236 95378376 99999527 66461920 此程序的運行時間為0.288308秒!
下面的代碼可以通過牛客網測試
#include<iostream> #include<algorithm> #include<vector> #include <cstdlib> using namespace std; struct point{ //定義結構體 int x,y; }; bool cmp(point a,point b){ //自定義排序方法 return a.y==b.y?a.x>b.x:a.y<b.y; //y升序,x降序 } point p[500001]; point filter[500001]; int main(){ int count; scanf("%d",&count); for(int i = 0; i < count; i++) { scanf("%d%d", &p[i].x, &p[i].y); } int curMaxRank = 0; int curMaxIndex = 0; for(int i=0;i<count;i++){ int temp =p[i].x+p[i].y-std::abs(p[i].x-p[i].y); if(temp > curMaxRank) { curMaxRank = temp; curMaxIndex = i; } } int fCount =0 ; for(int i=0;i<count;i++) { if(p[i].x >= p[curMaxIndex].x || p[i].y>= p[curMaxIndex].y) { filter[fCount++]=p[i]; } } sort(filter,filter+fCount,cmp); int maxx=-1; for(int i=fCount-1;i>=0;i--){ //y從大到小,若i點x值大於所有比其y值大的點的x值,那么i點為“最大點”。 if(filter[i].x>maxx){ printf("%d %d\n", filter[i].x, filter[i].y); maxx=filter[i].x; } } return 0; }
通過時間再200-300ms左右,和直接預排序的時間幾乎相同,那么應該是牛客網的測試用例都比較特別,過濾起不到大的效果或者是輸入輸出占用了大部分時間,無論怎么優化算法都沒辦法減少打印時間。。。不過沒關系,對於隨機用例我們可以肯定該算法會更加優秀。
總結與展望
對於隨機點進行大量測試,發現存在筆者給出的過濾方法,平均可以過濾99.9%的點。也就是說過濾后所剩點m的數量為原始點集n數量的千分之一。
使用過濾的額外好處是,我們只需要開辟千分之一的內存,然后就可以不改變原有點集的順序,也就是說如果題目還有不改變原有點集的要求,依然可以滿足 。
過濾付出的時間代價是線性的。那么算法的整體復雜度為O(n+ mlogm),而一般m值為n的千分之一。那么算法的平均復雜度為O(n),空間復雜度O(m)。通過上述代碼實際對比,性能提高了大約20倍左右。使用O(m)空間,可以確保不改變原有點集的順序。 可不可以繼續優化,可以可以,優化永無止境,只要別輕易放棄思考。
本文更新了使用四叉樹的數據結構來求解問題,對於隨機測試點也有不錯的性能。
如果對你有所幫助,點個贊唄~ 原創文章,請勿轉載,謝謝
