[英]Return Optimized x coordinates to normalize/maximize area for an array of rectangles with defined y positions
我已經包含了一個代碼片段,希望它能夠很好地總結事情,並以一種“填充空白”的狀態表示。
如果通過在更大的上下文中查看問題有助於理解問題的根源,那么我最終要做的是在手機上的日歷上顯示每日查看時間表,這可能類似於手機上日歷的工作方式。 當事件開始在時間上重疊時,這里由垂直 y 軸表示,我希望能夠優化這些事件的寬度和位置,而不是重疊它們並且不隱藏更多內容 - 但是當有太多許多能夠表明事情是隱藏的。
盡管示例是在 javascript 中,但我並沒有在尋找任何基於 CSS/HTML 的解決方案 - DOM 結構或多或少是一成不變的,只是尋找一種算法,如果它是在 C++ 中,可能會做我正在尋找的東西, TurboPascal、Assembly、Java,什么都不重要。 在我的示例中,預期結果越復雜,結果越像是粗略估計,並且在演示中呈現我的預期結果時也會出現這種情況 - 我什至沒有一個很好的方法來在腦海中進行數學計算一旦事情開始變得奇怪。
目標是填寫函數optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping = (rectangles,minimumArea,minimumWidth,xMin,xMax)=>{}
// Let's say I have an array like this of rectangles where they have set y coordinates // Some constraints on how this array comes out: it will besorted by the yTop property as shown below, but with no secondary sort criteria. // The rectangles difference between yBottom and yTop is always at least 15, in other words this array won't contain any rectangles less than 15 in height const rectanglesYcoordinatesOnlyExample1 = [ {"rectangle_id":"b22d","yTop":0,"yBottom":60}, {"rectangle_id":"8938","yTop":60,"yBottom":120}, {"rectangle_id":"e78a","yTop":60,"yBottom":120}, {"rectangle_id":"81ed","yTop":207,"yBottom":222}, {"rectangle_id":"b446","yTop":207,"yBottom":222}, {"rectangle_id":"ebd3","yTop":207,"yBottom":222}, {"rectangle_id":"2caf","yTop":208,"yBottom":223}, {"rectangle_id":"e623","yTop":227,"yBottom":242}, {"rectangle_id":"e6a3","yTop":270,"yBottom":320}, {"rectangle_id":"e613","yTop":272,"yBottom":460}, {"rectangle_id":"c2d1","yTop":272,"yBottom":290}, {"rectangle_id":"e64d","yTop":274,"yBottom":300}, {"rectangle_id":"b653","yTop":276,"yBottom":310}, {"rectangle_id":"e323","yTop":276,"yBottom":310}, {"rectangle_id":"fca3","yTop":300,"yBottom":315} ] // I want to get a result sort of like this, explanations provided, although I'm not sure if my internal calculations in my head are 100% on the further I go. // And I want to a run a function like so: // optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping(rectanglesYcoordinatesOnlyExample1,(33.3 * 15),10,0,100); // I will make this call later but I need to hoist my expected results here to enable the mocking to work for now at the point of the function definiton for my example. (see below) // like so I'd get a result something like this, but I start becoming less certain of what the correct result should be the more I go into fringe stuff. const expectedResultMoreOrLessForExample1 = [ {"rectangle_id":"b22d","leftX":0,"rightX":100,"yTop":0,"yBottom":60}, {"rectangle_id":"8938","leftX":0,"rightX":50,"yTop":60,"yBottom":120}, {"rectangle_id":"e78a","leftX":50,"rightX":100,"yTop":60,"yBottom":120}, {"rectangle_id":"81ed","leftX":0,"rightX":33.3,"yTop":207,"yBottom":222}, // Three rectangles side by side with minimum Area ["81ed","b446","ebd3"] from this point {"rectangle_id":"b446","leftX":33.3,"rightX":66.6,"yTop":207,"yBottom":222}, {"rectangle_id":"ebd3","isMax":true,"leftX":66.7,"rightX":100,"yTop":207,"yBottom":222}, // has isMax property because there would be an overlap if it tried the next result, and it can't take area away from the other rectangles // This rectangle gets thrown out because it would be there are 3 other rectangles in that area each with the minimum area (33.3 * 15); // {"rectangle_id":"2caf","yTop":208,"yBottom":223}, This one gets thrown out from the result the time being because there are too many rectangles in one area of vertical space. {"rectangle_id":"e623","yTop":227,"yBottom":242,"leftX":0,"rightX":100}, {"rectangle_id":"e6a3","leftX":0,"rightX":25,"yTop":270,"yBottom":320}, {"rectangle_id":"e613","leftX":25,"rightX":35,"yTop":272,"yBottom":460}, {"rectangle_id":"c2d1","leftX":71.28,"rightX":100,"yTop":272,"yBottom":290}, // fill the remaining space since optimizing to max area would take 99% {"rectangle_id":"e64d","leftX":35,"rightX":61.28,"yTop":274,"yBottom":300}, {"rectangle_id":"b653","yTop":276,"yBottom":940,"leftX":61.28,rightX:71.28}, {"rectangle_id":"fca3","leftX":35,"rightX":61.28,"yTop":300,"yBottom":315} ] // the function name is really long to reflect what it is what I want to do. Don't normally make functions this intense const optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping = (rectangles,minimumArea,minimumWidth,xMin,xMax)=>{ // TODO : fill in the optimization function. // Code I'm looking for would be swapped in here if you wanted to make changes to demo it do it here if(rectangles === rectanglesYcoordinatesOnlyExample1 && minimumArea === (33.3 * 15) && minimumWidth === 10 && xMin === 0 && xMax === 100){ // Just handling the example return expectedResultMoreOrLessForExample1; } else { console.log('I only know how to handle example 1, as computed by a human, poorly. fill in the function and replace the block with working stuff'); return []; } } const displayResults = (completedRectangleList) => { const rectangleColors = ['cyan','magenta','green','yellow','orange'] completedRectangleList.forEach((rectangle,index) =>{ let newRectangle = document.createElement('div'); newRectangle.style.position = 'absolute'; newRectangle.style.height = rectangle.yBottom - rectangle.yTop + 'px'; newRectangle.style.top = rectangle.yTop + 'px'; newRectangle.style.left = parseInt(rectangle.leftX)+'%'; newRectangle.style.width = rectangle.rightX - rectangle.leftX + "%"; newRectangle.style.backgroundColor = rectangleColors[index % rectangleColors.length]; newRectangle.innerHTML = rectangle.rectangle_id; if(rectangle.isMax){ newRectangle.innerHTML += '- more hidden'; } document.body.appendChild(newRectangle); }) } // I am calling this function with minimum Area of 33.3 * 15, because it represents 3 min height rectangles taking up a third of the minX,maxX values, which are 0 & 100, representing a percentage value ultimately let resultForExample1 = optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping(rectanglesYcoordinatesOnlyExample1,(33.3 * 15),10,0,100); displayResults(resultForExample1);
就我所嘗試的而言,我開始了一些事情,然后我想到了一些邊緣案例,事情變得有點混亂。 即使在我頭腦中計算出的預期結果中,我也認為我自己的人性化計算有點偏離,所以在評估這個問題並查看我的預期結果以及它的渲染時,它有點偏離。 希望optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping()
背后的意圖和意義或多或少是清楚的。
我仍在研究可能的方法,但同時也在尋求群眾的智慧,直到我想到。 我對解決方案很好奇,但我還沒有找到正確的軌道。
此答案中的算法嘗試在固定寬度的邊界框內排列矩形。 算法的輸入是指定topY
和bottomY
的矩形數組。 該算法計算每個矩形的leftX
和rightX
。 矩形的大小和位置可以避免重疊。 例如,下圖顯示了由算法排列的 7 個矩形。 在區域 1 中,最大重疊為 2,因此矩形繪制為半寬。 區域 2 沒有重疊,所以矩形是全寬的。 並且區域 3 有重疊 3,因此矩形是邊界框寬度的三分之一。
該算法分為三個主要步驟:
eventQueue
。 每個矩形產生兩個事件:帶有EVENT_START
的topY
和帶有EVENT_STOP
的bottomY
。 eventQueue
是一個優先級隊列,其中優先級基於定義事件的三個值(按所示順序計算):
y
:較低的y
值具有優先權。type
:EVENT_STOP 優先於 EVENT_START。rectID
:較低的rectID
值具有優先權。regionQueue
。 regionQueue
是一個簡單的 FIFO,它允許在確定區域范圍后再次處理事件。maxOverlap
(受函數參數限制)。 maxOverlap
確定區域內所有矩形的寬度。regionQueue
。 算法的這一部分使用了一個名為usedColumns
的數組。 該數組中的每個條目要么是-1
(表示該列未在使用中)要么是一個rectID
(表示哪個矩形正在使用該列)。 當從EVENT_START
彈出regionQueue
,會為矩形分配一列。 當從EVENT_STOP
彈出regionQueue
,該列返回到未使用 (-1) 狀態。 該算法的輸入是一個矩形數組。 這些矩形的topY
和bottomY
值必須由調用者填充。 leftX
和rightX
值必須初始化為 -1。 如果算法完成時 X 值仍為 -1,則無法為矩形分配位置,因為超出了overlapLimit
。 所有其他矩形都有完整的坐標集,可以繪制了。
typedef struct
{
int topY; // input parameter, filled in by the caller
int bottomY; // input parameter, filled in by the caller
int leftX; // output parameter, must be initially -1
int rightX; // output parameter, must be initially -1
}
stRect;
typedef struct
{
int y; // this is the 'topY' or 'bottomY' of a rectangle
int type; // either EVENT_START or EVENT_STOP
int rectID; // the index into the input array for this rectangle
}
stEvent;
enum { EVENT_START, EVENT_STOP };
void arrangeRectangles(stRect *rectArray, int length, int overlapLimit, int containerWidth)
{
stQueue *eventQueue = queueCreate();
stQueue *regionQueue = queueCreate();
// fill the event queue with START and STOP events for each rectangle
for (int i = 0; i < length; i++)
{
stEvent startEvent = {rectArray[i].topY, EVENT_START, i};
queueAdd(eventQueue, &startEvent);
stEvent stopEvent = {rectArray[i].bottomY, EVENT_STOP, i};
queueAdd(eventQueue, &stopEvent);
}
while (queueIsNotEmpty(eventQueue))
{
// search for the end of a region, while keeping track of the overlap in that region
int overlap = 0;
int maxOverlap = 0;
stEvent event;
while (queuePop(eventQueue, &event)) // take from the event queue
{
queueAdd(regionQueue, &event); // save in the region queue
if (event.type == EVENT_START)
overlap++;
else
overlap--;
if (overlap == 0) // reached the end of a region
break;
if (overlap > maxOverlap)
maxOverlap = overlap;
}
// limit the overlap as specified by the function parameter
if (maxOverlap > overlapLimit)
maxOverlap = overlapLimit;
// compute the width to be used for rectangles in this region
int width = containerWidth / maxOverlap;
// create and initialize an array to keep track of which columns are in use
int usedColumns[maxOverlap];
for (int i = 0; i < maxOverlap; i++)
usedColumns[i] = -1;
// process the region, computing left and right X values for each rectangle
while (queuePop(regionQueue, &event))
{
if (event.type == EVENT_START)
{
// find an available column for this rectangle, and assign the X values
for (int column = 0; column < maxOverlap; column++)
if (usedColumns[column] < 0)
{
usedColumns[column] = event.rectID;
rectArray[event.rectID].leftX = column * width;
rectArray[event.rectID].rightX = (column+1) * width;
break;
}
}
else
{
// free the column that's being used for this rectangle
for (int i = 0; i < maxOverlap; i++)
if (usedColumns[i] == event.rectID)
{
usedColumns[i] = -1;
break;
}
}
}
}
queueDestroy(eventQueue);
queueDestroy(regionQueue);
}
void example(void)
{
stRect inputArray[] = {
{ 0,150,-1,-1},
{ 30,180,-1,-1},
{180,360,-1,-1},
{360,450,-1,-1},
{420,540,-1,-1},
{450,570,-1,-1},
{480,540,-1,-1}
};
int length = sizeof(inputArray) / sizeof(inputArray[0]);
arrangeRectangles(inputArray, length, 3, 100);
}
注意:我對這段代碼的有效性不做任何聲明。 徹底的審查和測試留給讀者作為練習。
@chairmanmow慷慨地將算法翻譯成 javascript,以節省其他人尋找 javascript 解決方案的時間。 這是翻譯:
const topZero = 0; const parent = i => ((i + 1) >>> 1) - 1; const left = i => (i << 1) + 1; const right = i => (i + 1) << 1; class PriorityQueue { constructor(comparator = (a, b) => a > b) { this._heap = []; this._comparator = comparator; } size() { return this._heap.length; } isEmpty() { return this.size() == 0; } peek() { return this._heap[topZero]; } push(...values) { values.forEach(value => { this._heap.push(value); this._siftUp(); }); return this.size(); } pop() { const poppedValue = this.peek(); const bottom = this.size() - 1; if (bottom > topZero) { this._swap(topZero, bottom); } this._heap.pop(); this._siftDown(); return poppedValue; } replace(value) { const replacedValue = this.peek(); this._heap[topZero] = value; this._siftDown(); return replacedValue; } _greater(i, j) { return this._comparator(this._heap[i], this._heap[j]); } _swap(i, j) { [this._heap[i], this._heap[j]] = [this._heap[j], this._heap[i]]; } _siftUp() { let node = this.size() - 1; while (node > topZero && this._greater(node, parent(node))) { this._swap(node, parent(node)); node = parent(node); } } _siftDown() { let node = topZero; while ( (left(node) < this.size() && this._greater(left(node), node)) || (right(node) < this.size() && this._greater(right(node), node)) ) { let maxChild = (right(node) < this.size() && this._greater(right(node), left(node))) ? right(node) : left(node); this._swap(node, maxChild); node = maxChild; } } } const rectangles = [{ rectID: "b22d", "yTop": 0, "yBottom": 60, "leftX": -1, "rightX": -1 }, { rectID: "8938", "yTop": 60, "yBottom": 120, "leftX": -1, "rightX": -1 }, { rectID: "e78a", "yTop": 60, "yBottom": 120, "leftX": -1, "rightX": -1 }, { rectID: "81ed", "yTop": 207, "yBottom": 222, "leftX": -1, "rightX": -1 }, { rectID: "b446", "yTop": 207, "yBottom": 222, "leftX": -1, "rightX": -1 }, { rectID: "ebd3", "yTop": 207, "yBottom": 222, "leftX": -1, "rightX": -1 }, { rectID: "2caf", "yTop": 208, "yBottom": 223, "leftX": -1, "rightX": -1 }, { rectID: "e623", "yTop": 227, "yBottom": 242, "leftX": -1, "rightX": -1 }, { rectID: "e6a3", "yTop": 270, "yBottom": 320, "leftX": -1, "rightX": -1 }, { rectID: "e613", "yTop": 272, "yBottom": 460, "leftX": -1, "rightX": -1 }, { rectID: "c2d1", "yTop": 272, "yBottom": 290, "leftX": -1, "rightX": -1 }, { rectID: "e64d", "yTop": 274, "yBottom": 300, "leftX": -1, "rightX": -1 }, { rectID: "b653", "yTop": 276, "yBottom": 310, "leftX": -1, "rightX": -1 }, { rectID: "e323", "yTop": 276, "yBottom": 310, "leftX": -1, "rightX": -1 }, { rectID: "fca3", "yTop": 300, "yBottom": 315, "leftX": -1, "rightX": -1 } ]; let eventQueue = new PriorityQueue((a, b) => { if (ay !== by) return ay < by; if (a.type !== b.type) return a.type > b.type; return a.rectID > b.rectID; }) let regionQueue = []; // FIFO const EVENT_START = 0; const EVENT_STOP = 1; const queueAdd = (queue, toAdd, type, priority) => { return queue.push(toAdd); } const queuePop = (queue) => { return queue.pop(); } const queueDestroy = (queue) => { return queue = []; } const optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping = (rectArray, length, overlapLimit, containerWidth) => { // fill the event queue with START and STOP events for each rectangle for (let i = 0; i < length; i++) { let startEvent = { y: rectArray[i].yTop, type: EVENT_START, index: i }; eventQueue.push(startEvent); let stopEvent = { y: rectArray[i].yBottom, type: EVENT_STOP, index: i }; eventQueue.push(stopEvent); } while (eventQueue.size()) { // queueIsNotEmpty(eventQueue) // search for the end of a region, while keeping track of the overlap in that region let overlap = 0; let maxOverlap = 0; let event; while (event = eventQueue.pop()) { // take from the event queue queueAdd(regionQueue, event); // save in the region queue if (event.type === 0) { overlap++; } else { overlap--; } if (overlap === 0) { // reached the end of a region break; } // if we have a new maximum for the overlap, update 'maxOverlap' if (overlap > maxOverlap) { maxOverlap = overlap; } } // limit the overlap as specified by the function parameter if (maxOverlap > overlapLimit) { maxOverlap = overlapLimit; } // compute the width to be used for rectangles in this region const width = parseInt(containerWidth / maxOverlap); // create and initialize an array to keep track of which columns are in use let usedColumns = new Array(maxOverlap); for (let i = 0; i < maxOverlap; i++) { if (usedColumns[i] == event.rectID) { usedColumns[i] = -1; break; } } // process the region, computing left and right X values for each rectangle while (event = queuePop(regionQueue)) { if (event.type == 0) { // find an available column for this rectangle, and assign the X values for (let column = 0; column < maxOverlap; column++) { if (usedColumns[column] < 0) { usedColumns[column] = event.rectID; rectArray[event.index].leftX = column * width; rectArray[event.index].rightX = (column + 1) * width; break; } } } else { // free the column that's being used for this rectangle for (let i = 0; i < maxOverlap; i++) if (usedColumns[i] == event.rectID) { usedColumns[i] = -1; break; } } } } return rectArray; } const displayResults = (completedRectangleList) => { const rectangleColors = ['cyan', 'magenta', 'green', 'yellow', 'orange'] completedRectangleList.forEach((rectangle, index) => { if (rectangle.leftX > -1 && rectangle.rightX > -1) { let newRectangle = document.createElement('div'); newRectangle.style.position = 'absolute'; newRectangle.style.height = rectangle.yBottom - rectangle.yTop + 'px'; newRectangle.style.top = rectangle.yTop + 'px'; newRectangle.style.left = parseInt(rectangle.leftX) + '%'; newRectangle.style.width = rectangle.rightX - rectangle.leftX + "%"; newRectangle.style.backgroundColor = rectangleColors[index % rectangleColors.length]; newRectangle.innerHTML = rectangle.rectID; if (rectangle.isMax) { newRectangle.innerHTML += '- more hidden'; } document.body.appendChild(newRectangle); } }) } let results = optimizeLeftAndRightXStartPointsForNormalizedRectangleAreaWithoutOverlapping(rectangles, (rectangles.length), 3, 100); console.log('results ' + JSON.stringify(results)); displayResults(results);
下面是java代碼
public class Main {
public static void main(String[] args) {
ArrayList<stRect> inputArray = new ArrayList<>();
inputArray.add(new stRect( 0,150,-1,-1));
inputArray.add(new stRect( 30,180,-1,-1));
inputArray.add(new stRect( 180,360,-1,-1));
inputArray.add(new stRect( 360,450,-1,-1));
inputArray.add(new stRect( 420,540,-1,-1));
inputArray.add(new stRect( 450,570,-1,-1));
inputArray.add(new stRect( 480,540,-1,-1));
arrangeRectangles(inputArray, inputArray.size(), 3, 100);
for(int i = 0;i<inputArray.size();i++){
System.out.println(inputArray.get(i).topY+" "+inputArray.get(i).bottomY+" "+inputArray.get(i).leftX+" "+inputArray.get(i).rightX);
}
}
private static void arrangeRectangles(ArrayList<stRect>rectArray, int length, int overlapLimit, int containerWidth){
int EVENT_START = 0, EVENT_STOP = 1;
PriorityQueue<stEvent> eventQueue = new PriorityQueue<>(new MyComparator());
Queue<stEvent> regionQueue = new LinkedList<>();
for (int i = 0; i < length; i++) {
stEvent startEvent = new stEvent(rectArray.get(i).topY,EVENT_START, i);
eventQueue.add(startEvent);
stEvent stopEvent = new stEvent(rectArray.get(i).bottomY,EVENT_STOP, i);
eventQueue.add(stopEvent);
}
while (!eventQueue.isEmpty()){
int overlap = 0;
int maxOverlap = 0;
stEvent event;
while (!eventQueue.isEmpty()){ // take from the event queue
event = eventQueue.remove();
regionQueue.add(event); // save in the region queue
if (event.type == EVENT_START)
overlap++;
else
overlap--;
if (overlap == 0) // reached the end of a region
break;
if (overlap > maxOverlap)
maxOverlap = overlap;
}
// limit the overlap as specified by the function parameter
if (maxOverlap > overlapLimit)
maxOverlap = overlapLimit;
// compute the width to be used for rectangles in this region
int width = containerWidth / maxOverlap;
int usedColumns[] = new int[maxOverlap];
for (int i = 0; i < maxOverlap; i++)
usedColumns[i] = -1;
while (!regionQueue.isEmpty()){
event = regionQueue.remove();
if (event.type == EVENT_START) {
// find an available column for this rectangle, and assign the X values
for (int column = 0; column < maxOverlap; column++){
if (usedColumns[column] < 0) {
usedColumns[column] = event.rectID;
rectArray.get(event.rectID).leftX = column * width;
rectArray.get(event.rectID).rightX = (column+1) * width;
break;
}
}
}else {
// free the column that's being used for this rectangle
for (int i = 0; i < maxOverlap; i++){
if (usedColumns[i] == event.rectID)
{
usedColumns[i] = -1;
break;
}
}
}
}
}
eventQueue.clear();
regionQueue.clear();
}
}
public class MyComparator implements Comparator<stEvent> {
@Override
public int compare(stEvent o1, stEvent o2) {
if(o1.y<o2.y)
return -1;
if(o1.y>o2.y)
return 1;
if(o1.type == 0 && o2.type ==1)
return 1;
if(o1.type == 1 && o2.type ==0)
return -1;
if(o1.rectID<o2.rectID)
return -1;
if(o1.rectID>o2.rectID)
return 1;
return 0;
}
}
class stEvent {
int y;
int type;
int rectID;
stEvent(int y, int type, int rectID) {
this.y = y;
this.type = type;
this.rectID = rectID;
}
}
class stRect {
int topY; // input parameter, filled in by the caller
int bottomY; // input parameter, filled in by the caller
int leftX; // output parameter, must be initially -1
int rightX;
stRect(int topY, int bottomY, int leftX, int rightX) {
this.topY = topY;
this.bottomY = bottomY;
this.leftX = leftX;
this.rightX = rightX;
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.