我在最近的一次采访中发现了这一点。
我们给出了一个由数字组成的N * M网格,网格中的路径是你遍历的节点。我们得到一个约束,我们只能在网格中向右或向下移动。所以给定这个网格,我们需要找到排序后的词典最小路径,从网格的左上角到右下角到达
EG。如果网格是2 * 2
4 3
5 1
那么根据问题的词汇最小路径是" 1 3 4"。
怎么办这样的问题?代码表示赞赏。提前谢谢。
答案 0 :(得分:4)
您可以使用Dynamic programming来解决此问题。让f(i, j)
成为从(i, j)
到(N, M)
仅向右和向下移动的最小字典路径(在排序路径之后)。请考虑以下重复:
f(i, j) = sort( a(i, j) + smallest(f(i + 1, j), f(i, j + 1)))
其中a(i, j)
是(i, j)
网格中的值,smallest (x, y)
返回x
和y
之间较小的词典字符串。 +
连接两个字符串,sort(str)
按字符顺序对字符串str
进行排序。
重复的基本情况是:
f(N, M) = a(N, M)
i = N
或j = M
时也会发生重复更改(确保您看到了这一点)。
考虑以下C++
编写的代码:
//-- the 200 is just the array size. It can be modified
string a[200][200]; //-- represent the input grid
string f[200][200]; //-- represent the array used for memoization
bool calculated[200][200]; //-- false if we have not calculate the value before, and true if we have
int N = 199, M = 199; //-- Number of rows, Number of columns
//-- sort the string str and return it
string srt(string &str){
sort(str.begin(), str.end());
return str;
}
//-- return the smallest of x and y
string smallest(string & x, string &y){
for (int i = 0; i < x.size(); i++){
if (x[i] < y[i]) return x;
if (x[i] > y[i]) return y;
}
return x;
}
string solve(int i, int j){
if (i == N && j == M) return a[i][j]; //-- if we have reached the buttom right cell (I assumed the array is 1-indexed
if (calculated[i][j]) return f[i][j]; //-- if we have calculated this before
string ans;
if (i == N) ans = srt(a[i][j] + solve(i, j + 1)); //-- if we are at the buttom boundary
else if (j == M) ans = srt(a[i][j] + solve(i + 1, j)); //-- if we are at the right boundary
else ans = srt(a[i][j] + smallest(solve(i, j + 1), solve(i + 1, j)));
calculated[i][j] = true; //-- to fetch the calculated result in future calls
f[i][j] = ans;
return ans;
}
string calculateSmallestPath(){
return solve(1, 1);
}
答案 1 :(得分:1)
您可以应用动态编程方法在O(N * M * (N + M))
时间和空间复杂度中解决此问题。
在下面我要考虑,N
是行数,M
是列数,左上角的单元格有坐标(0, 0)
,首先是行和第二列。
允许每个单元格存储按字典顺序排列的按字典顺序排列的最小路径。具有0
索引的行和列的答案是微不足道的,因为只有一种方法可以到达每个单元格。对于其余的单元格,您应该选择顶部和左侧单元格的最小路径,并插入当前单元格的值。
算法是:
path[0][0] <- a[0][0]
path[i][0] <- insert(a[i][0], path[i - 1][0])
path[0][j] <- insert(a[0][j], path[0][j - 1])
path[i][j] <- insert(a[i][j], min(path[i - 1][j], path[i][j - 1])
答案 2 :(得分:1)
如果没有重复数字,也可以在O (NM log (NM))
中实现。
<强>直觉:强>
假设我将左上角(a,b)
和右下角(c,d)
的网格标记为G(a,b,c,d)
。由于您要获得按字典顺序排列的最小字符串 AFTER 对路径进行排序,因此目标应该是每次在G
中找到最小值。如果达到这个最小值,比如(i,j)
,那么G(i,b,c,j)
和G(a,j,i,d)
将无法用于搜索我们的下一分钟(路径)。也就是说,我们所希望的路径的值永远不会出现在这两个网格中。证明?如果遍历这些网格中的任何位置,则不会让我们达到G(a,b,c,d)
中的最小值((i,j)
处的最小值)。而且,如果我们避免(i,j)
,我们构建的路径不能在字典上最小。
所以,首先我们找到G(1,1,m,n)
的分钟。假设它在(i,j)
。标记分钟。然后,我们在G(1,1,i,j)
和G(i,j,m,n)
中找到 min ,并对它们执行相同操作。继续这样,直到最后,我们有m+n-1
个标记的条目,这将构成我们的路径。线性遍历原始网格G(1,1,m,n)
,如果标记了值,则报告值。
<强>方法强>
每次在G
中查找分数都很昂贵。如果我们将网格中的每个值映射到它的位置怎么办? - 遍历网格并维护字典Dict
,其中键为(i,j)
处的值,值为元组(i,j)
。最后,您将获得一个涵盖网格中所有值的键值对列表。
现在,我们将维护一个有效网格列表,我们将在其中找到我们路径的候选者。第一个有效网格为G(1,1,m,n)
。
对键进行排序,并从排序键集S
中的第一个值开始迭代。
维护一个有效网格树T(G)
,以便每个G(a,b,c,d) in T
,G.left = G(a,b,i,j)
和G.right = G(i,j,c,d)
(i,j) = location of min val in G(a,b,c,d)
现在算法:
for each val in sorted key set S do
(i,j) <- Dict(val)
Grid G <- Root(T)
do while (i,j) in G
if G has no child do
G.left <- G(a,b,i,j)
G.right <- G(i,j,c,d)
else if (i,j) in G.left
G <- G.left
else if (i,j) in G.right
G <- G.right
else
dict(val) <- null
end do
end if-else
end do
end for
for each val in G(1,1,m,n)
if dict(val) not null
solution.append(val)
end if
end for
return solution
Java代码:
class Grid{
int a, b, c, d;
Grid left, right;
Grid(int a, int b, int c, int d){
this.a = a;
this.b = b;
this.c = c;
this.d = d;
left = right = null;
}
public boolean isInGrid(int e, int f){
return (e >= a && e <= c && f >= b && f <= d);
}
public boolean hasNoChild(){
return (left == null && right == null);
}
}
public static int[] findPath(int[][] arr){
int row = arr.length;
int col = arr[0].length;
int[][] index = new int[row*col+1][2];
HashMap<Integer,Point> map = new HashMap<Integer,Point>();
for(int i = 0; i < row; i++){
for(int j = 0; j < col; j++){
map.put(arr[i][j], new Point(i,j));
}
}
Grid root = new Grid(0,0,row-1,col-1);
SortedSet<Integer> keys = new TreeSet<Integer>(map.keySet());
for(Integer entry : keys){
Grid temp = root;
int x = map.get(entry).x, y = map.get(entry).y;
while(temp.isInGrid(x, y)){
if(temp.hasNoChild()){
temp.left = new Grid(temp.a,temp.b,x, y);
temp.right = new Grid(x, y,temp.c,temp.d);
break;
}
if(temp.left.isInGrid(x, y)){
temp = temp.left;
}
else if(temp.right.isInGrid(x, y)){
temp = temp.right;
}
else{
map.get(entry).x = -1;
break;
}
}
}
int[] solution = new int[row+col-1];
int count = 0;
for(int i = 0 ; i < row; i++){
for(int j = 0; j < col; j++){
if(map.get(arr[i][j]).x >= 0){
solution[count++] = arr[i][j];
}
}
}
return solution;
}
空间复杂性由字典 - O(NM)
和树 - O(N+M)
的维护构成。总体而言:O(NM)
填写然后对字典进行排序的时间复杂度 - O(NM log(NM))
;用于检查每个NM
值的树 - O(NM log(N+M))
。总体而言 - O(NM log(NM))
。
当然,如果重复这些值,这将不起作用,因为我们对网格中的单个值有多个(i,j)
,并且决定选择哪个不再满足贪婪的做法。
额外的FYI:我之前听到的与此类似的问题有一个额外的网格属性 - 没有值重复且数字来自1 to NM
。在这种情况下,复杂性可能会进一步降低到O(NM log(N+M))
,因为您可以简单地使用网格中的值作为数组的索引(不需要排序)。