使用手机键盘生成10位数字

24
给定下面所示的手机按键:

Given a phone keypad as shown below:

1 2 3
4 5 6
7 8 9
  0
从数字1开始,按照象棋游戏中马的移动方式,可以形成多少个不同的10位数字?例如,如果我们在数字1处,则下一个数字可以是6或8;如果我们在数字6处,则下一个数字可以是1、7或0。允许数字重复 - 1616161616 是一个有效的数字。是否有一个多项式时间算法来解决这个问题?该问题要求我们只给出10位数字的计数,而不一定要列出数字。编辑:我尝试将其建模为一个图,其中每个数字都有2或3个数字作为其邻居。然后,我使用DFS遍历到第10个节点,每次到达深度为10时增加数字的计数。显然,这不是多项式时间。假设每个数字只有2个邻居,这将需要至少2^10次迭代。变量是数字的数量,我已经采用了10位数字的例子,它也可以是n位数字。

7
作业?你已经尝试过什么了吗? - Eric J.
2
如果这确实是一份作业,那么您最好将其标记为作业,并展示出您已经做过的努力以及卡住了哪里。大多数人并不想替您完成作业,即使有人这样做了,您也不会学到任何东西。 - Eric J.
1
是的,确实如此。这里的变量是数字的位数。在这里,n = 10位数字。将更新问题以使其更清晰。 - srikanta
2
5永远无法到达,因此在这个特定问题中可以安全地忽略它,而不会对解决方案产生任何影响。 - srikanta
2
想要检查自己的解决方案的人,正确答案是1424。 - Sarp Centel
显示剩余5条评论
12个回答

42

当然可以用多项式时间完成。这是一个很好的动态规划记忆化搜索练习。

让我们假设N(数字的数量)等于10,以此作为例子。

可以这样递归地思考:我可以使用10个数字从1开始构造多少个数字?

答案是

[number of 9-digit numbers starting from 8] +
[number of 9-digit numbers starting from 6].

那么有多少个以8开头的九位数字呢?嗯,

[number of 8-digit numbers starting from 1] +
[number of 8-digit numbers starting from 3]

等等。当你得到问题"从X开始有多少个一位数"时,基本情况已经达成(答案显然是1)。

在复杂性方面,关键观察是您可以重用以前计算的解决方案。例如,回答"从3开始有多少个五位数"的答案,既可以用于回答"从8开始有多少个六位数",也可以用于回答"从4开始有多少个六位数"。这种重用将复杂性从指数级降至多项式级别。

让我们更仔细地看一下动态规划解决方案的复杂性:

这样的实现会以以下方式填充矩阵:

num[1][i] = 1, for all 0<=i<=9   -- there are one 1-digit number starting from X.

for digits = 2...N
    for from = 0...9
        num[digits][from] = num[digits-1][successor 1 of from] +
                            num[digits-1][successor 2 of from] +
                            ...
                            num[digits-1][successor K of from]

return num[N][1]                 -- number of N-digit numbers starting from 1.

该算法只是逐个填充矩阵的单元格,矩阵的维度为10*N,因此运行时间为线性时间。

我凭记忆写下了这些内容,如果有任何错误,请纠正。


使用Python3编写的工作解决方案,包含您的算法,可在https://github.com/harishvc/challenges/blob/master/dp-knight-chess-movement.py找到。您能进一步解释一下线性时间复杂度吗? - harishvc
1
@aioobe,由于我们基于先前的行在缓存中计算当前行,因此我们只需使用int [10],即可获得O(1)空间。 - Konstantin Milyutin

2
我决定解决这个问题并尽可能地使其可扩展。该解决方案允许您:
定义自己的棋盘(电话键盘,国际象棋棋盘等)。
定义自己的棋子(骑士,车,象等);您将需要编写具体类并从工厂生成它。
通过一些有用的实用程序方法检索几个信息。
以下是各个类:
PadNumber:定义电话键盘上的按钮的类。可以重命名为“Square”以表示一个棋盘方格。
ChessPiece:抽象类,定义所有棋子的字段。
Movement:接口,定义移动方法并允许生成棋子。
PieceFactory:生成棋子的工厂类。
Knight:继承自ChessPiece并实现Movement的具体类。
PhoneChess:入口类。
Driver:驱动代码。
好了,下面是代码 :)
package PhoneChess;

import java.awt.Point;

public class PadNumber {

private String number = "";
private Point coordinates = null;

public PadNumber(String number, Point coordinates)
{
    if(number != null && number.isEmpty()==false)
        this.number = number;
    else
        throw new IllegalArgumentException("Input cannot be null or empty.");

    if(coordinates == null || coordinates.x < 0 || coordinates.y < 0)
        throw new IllegalArgumentException();
    else
        this.coordinates = coordinates;

}

public String getNumber()
{
    return this.number;
}
public Integer getNumberAsNumber()
{
    return Integer.parseInt(this.number);
}

public Point getCoordinates()
{
    return this.coordinates;
}
public int getX()
{
    return this.coordinates.x;
}
public int getY()
{
    return this.coordinates.y;
}

}

棋子

package PhoneChess;

import java.util.HashMap;
import java.util.List;

public abstract class ChessPiece implements Movement {

protected String name = "";
protected HashMap<PadNumber, List<PadNumber>> moves = null;
protected Integer fullNumbers = 0;
protected int[] movesFrom = null;
protected PadNumber[][] thePad = null;
}

移动界面:

package PhoneChess;

import java.util.List;

public interface Movement 
{
public Integer findNumbers(PadNumber start, Integer digits);
public abstract boolean canMove(PadNumber from, PadNumber to);
public List<PadNumber> allowedMoves(PadNumber from);
public Integer countAllowedMoves(PadNumber from);
}

PieceFactory

package PhoneChess;

public class PieceFactory 
{
    public ChessPiece getPiece(String piece, PadNumber[][] thePad)
    {
    if(thePad == null || thePad.length == 0 || thePad[0].length == 0)
        throw new IllegalArgumentException("Invalid pad");
    if(piece == null)
        throw new IllegalArgumentException("Invalid chess piece");

    if(piece.equalsIgnoreCase("Knight"))
        return new Knight("Knight", thePad);
    else
        return null;
}
}

骑士职业
package PhoneChess;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public final class Knight extends ChessPiece implements Movement {

/**Knight movements
 * One horizontal, followed by two vertical
 * Or 
 * One vertical, followed by two horizontal
 * @param name
 */

public Knight(String name, PadNumber[][] thePad)
{
    if(name == null || name.isEmpty() == true)
        throw new IllegalArgumentException("Name cannot be null or empty");

    this.name = name;
    this.thePad = thePad;
    this.moves = new HashMap<>();
}


private Integer fullNumbers = null;

@Override
public Integer findNumbers(PadNumber start, Integer digits) 
{
    if(start == null || "*".equals(start.getNumber()) || "#".equals(start.getNumber()) ) { throw new IllegalArgumentException("Invalid start point"); }
    if(start.getNumberAsNumber() == 5) { return 0; } //Consider adding an 'allowSpecialChars' condition
    if(digits == 1) { return 1; };

    //Init
    this.movesFrom = new int[thePad.length * thePad[0].length];
    for(int i = 0; i < this.movesFrom.length; i++)
        this.movesFrom[i] = -1;

    fullNumbers = 0;
    findNumbers(start, digits, 1);      
    return fullNumbers;
}

private void findNumbers(PadNumber start, Integer digits, Integer currentDigits)
{
    //Base condition
    if(currentDigits == digits)
    {
        //Reset
        currentDigits = 1; 
        fullNumbers++; 
        return; 
    }
    if(!this.moves.containsKey(start))
        allowedMoves(start);

    List<PadNumber> options = this.moves.get(start);
    if(options != null)
    {
        currentDigits++; //More digits to be got
        for(PadNumber option : options)
            findNumbers(option, digits, currentDigits);
    }
}

@Override
public boolean canMove(PadNumber from, PadNumber to) 
{
    //Is the moves list available?
    if(!this.moves.containsKey(from.getNumber()))
    {
        //No? Process.
        allowedMoves(from);
    }
    if(this.moves.get(from) != null)
    {
        for(PadNumber option : this.moves.get(from))
        {
            if(option.getNumber().equals(to.getNumber()))
                return true;
        }
    }
    return false;

}

/***
 * Overriden method that defines each Piece's movement restrictions.
 */
@Override
public List<PadNumber> allowedMoves(PadNumber from) 
{
    //First encounter
    if(this.moves == null)
        this.moves = new HashMap<>();


    if(this.moves.containsKey(from))
        return this.moves.get(from);
    else
    {
        List<PadNumber> found = new ArrayList<>();
        int row = from.getY();//rows
        int col = from.getX();//columns

        //Cases:
        //1. One horizontal move each way followed by two vertical moves each way
        if(col-1 >= 0 && row-2 >= 0)//valid
        {
            if(thePad[row-2][col-1].getNumber().equals("*") == false && 
                    thePad[row-2][col-1].getNumber().equals("#") == false)
            {
                found.add(thePad[row-2][col-1]);
                this.movesFrom[from.getNumberAsNumber()] = this.movesFrom[from.getNumberAsNumber()] + 1;
            }

        }
        if(col-1 >= 0 && row+2 < thePad.length)//valid
        {
            if(thePad[row+2][col-1].getNumber().equals("*") == false && 
                    thePad[row+2][col-1].getNumber().equals("#") == false)
            {
                found.add(thePad[row+2][col-1]);
                this.movesFrom[from.getNumberAsNumber()] = this.movesFrom[from.getNumberAsNumber()] + 1;
            }
        }
        if(col+1 < thePad[0].length && row+2 < thePad.length)//valid
        {
            if(thePad[row+2][col+1].getNumber().equals("*") == false && 
                    thePad[row+2][col+1].getNumber().equals("#") == false)
            {
                found.add(thePad[row+2][col+1]);
                this.movesFrom[from.getNumberAsNumber()] = this.movesFrom[from.getNumberAsNumber()] + 1;
            }
        }
        if(col+1 < thePad[0].length && row-2 >= 0)//valid
        {
            if(thePad[row-2][col+1].getNumber().equals("*") == false && 
                    thePad[row-2][col+1].getNumber().equals("#") == false)
            found.add(thePad[row-2][col+1]);
        }
        //Case 2. One vertical move each way follow by two horizontal moves each way

        if(col-2 >= 0 && row-1 >= 0)
        {
            if(thePad[row-1][col-2].getNumber().equals("*") == false && 
                    thePad[row-1][col-2].getNumber().equals("#") == false)
            found.add(thePad[row-1][col-2]);
        }
        if(col-2 >= 0 && row+1 < thePad.length)
        {
            if(thePad[row+1][col-2].getNumber().equals("*") == false && 
                    thePad[row+1][col-2].getNumber().equals("#") == false)
            found.add(thePad[row+1][col-2]);
        }

        if(col+2 < thePad[0].length && row-1 >= 0)
        {
            if(thePad[row-1][col+2].getNumber().equals("*") == false && 
                    thePad[row-1][col+2].getNumber().equals("#") == false)
            found.add(thePad[row-1][col+2]);
        }
        if(col+2 < thePad[0].length && row+1 < thePad.length)
        {
            if(thePad[row+1][col+2].getNumber().equals("*") == false && 
                    thePad[row+1][col+2].getNumber().equals("#") == false)
            found.add(thePad[row+1][col+2]);
        }

        if(found.size() > 0)
        {
            this.moves.put(from, found);
            this.movesFrom[from.getNumberAsNumber()] = found.size();
        }
        else
        {
            this.moves.put(from, null); //for example the Knight cannot move from 5 to anywhere
            this.movesFrom[from.getNumberAsNumber()] = 0;
        }
    }

    return this.moves.get(from);


}

@Override
public Integer countAllowedMoves(PadNumber from) 
{
    int start = from.getNumberAsNumber();

    if(movesFrom[start] != -1)
        return movesFrom[start];
    else
    {
        movesFrom[start] = allowedMoves(from).size();
    }
    return movesFrom[start];
}

@Override
public String toString()
{
    return this.name;
}

}

PhoneChess参赛者类

package PhoneChess;


public final class PhoneChess 
{
private ChessPiece thePiece = null;
private PieceFactory factory = null;

public ChessPiece ThePiece()
{
    return this.thePiece;
}

public PhoneChess(PadNumber[][] thePad, String piece)
{
    if(thePad == null || thePad.length == 0 || thePad[0].length == 0)
        throw new IllegalArgumentException("Invalid pad");
    if(piece == null)
        throw new IllegalArgumentException("Invalid chess piece");

    this.factory = new PieceFactory();
    this.thePiece = this.factory.getPiece(piece, thePad);
}

public Integer findPossibleDigits(PadNumber start, Integer digits)
{
    if(digits <= 0)
        throw new IllegalArgumentException("Digits cannot be less than or equal to zero");

    return thePiece.findNumbers(start, digits);
}

public boolean isValidMove(PadNumber from, PadNumber to)
{
    return this.thePiece.canMove(from, to);
}

}

驱动程序代码:

public static void main(String[] args) {


    PadNumber[][] thePad = new PadNumber[4][3];
    thePad[0][0] = new PadNumber("1", new Point(0,0));
    thePad[0][1] = new PadNumber("2", new Point(1,0));
    thePad[0][2] = new PadNumber("3",new Point(2,0));
    thePad[1][0] = new PadNumber("4",new Point(0,1));
    thePad[1][1] = new PadNumber("5",new Point(1,1));
    thePad[1][2] = new PadNumber("6", new Point(2,1));
    thePad[2][0] = new PadNumber("7", new Point(0,2));
    thePad[2][1] = new PadNumber("8", new Point(1,2));
    thePad[2][2] = new PadNumber("9", new Point(2,2));
    thePad[3][0] = new PadNumber("*", new Point(0,3));
    thePad[3][1] = new PadNumber("0", new Point(1,3));
    thePad[3][2] = new PadNumber("#", new Point(2,3));

    PhoneChess phoneChess = new PhoneChess(thePad, "Knight");
    System.out.println(phoneChess.findPossibleDigits(thePad[0][1],4));
}

}

1
运行时常数时间解决方案:
#include <iostream>

constexpr int notValid(int x, int y) {
return !(( 1 == x && 3 == y ) || //zero on bottom.
         ( 0 <= x && 3 > x && //1-9
           0 <= y && 3 > y ));
}

class Knight {
    template<unsigned N > constexpr int move(int x, int y) {
        return notValid(x,y)? 0 : jump<N-1>(x,y);
    }

    template<unsigned N> constexpr int jump( int x, int y ) {
        return  move<N>(x+1, y-2) +
            move<N>(x-1, y-2) +
            move<N>(x+1, y+2) +
            move<N>(x-1, y+2) +
            move<N>(x+2, y+1) +
            move<N>(x-2, y+1) +
            move<N>(x+2, y-1) +
            move<N>(x-2, y-1);
    }

public:
    template<unsigned N> constexpr int count() {
        return move<N-1>(0,1) + move<N-1>(0,2) +
            move<N-1>(1,0) + move<N-1>(1,1) + move<N-1>(1,2) +
            move<N-1>(2,0) + move<N-1>(2,1) + move<N-1>(2,2);
    }
};

template<> constexpr int Knight::move<0>(int x, int y) { return notValid(x,y)? 0 : 1; }
template<> constexpr int Knight::count<0>() { return 0; } //terminal cases.
template<> constexpr int Knight::count<1>() { return 8; }


int main(int argc, char* argv[]) {
    static_assert( ( 16 == Knight().count<2>() ), "Fail on test with 2 lenght" );  // prof of performance
    static_assert( ( 35 == Knight().count<3>() ), "Fail on test with 3 lenght" );

    std::cout<< "Number of valid Knight phones numbers:" << Knight().count<10>() << std::endl;
    return 0;
}

如果您直接在代码中输入数字的位数(10),那么您可以直接执行 std::cout << 1424 << std::endl;。:-)(我假设如果您从 stdin 读取 N,则此解决方案无法使用。) - aioobe
@aioobe 如果你将N输入编译器的标准输入,它应该可以工作 :) - VladimirS

1

该方法返回以数字 1 开头的 10 位数字列表。再次,计数为 1424。

  public ArrayList<String> getList(int digit, int length, String base ){
    ArrayList<String> list = new ArrayList<String>();
    if(length == 1){
        list.add(base);
        return list;
    }
    ArrayList<String> temp;

    for(int i : b[digit]){
        String newBase = base +i;
        list.addAll(getList(i, length -1, newBase ));
    }
    return list;
}

1
我不确定是否漏掉了什么,但是通过阅读问题描述,我得出了这个解决方案。它具有O(n)时间复杂度和O(1)空间复杂度。
我想数字1在一个角落里,对吧?在每个角落,你可以移动到其中一个侧面(从9和3的4或6,或从7和1的6)或一个“垂直”的侧面(从3和1的8或从9和7的2)。所以,角落会增加两次移动:一次侧面移动和一次“垂直”移动。这对于所有四个角落(1、3、9、7)都是正确的。
从每个侧面,你可以移动到两个角落(从6的7和1,4的9和3)或者你可以到达底部按键(0)。那就是三个步骤。两个角落和一个底部。
在底部按键(0)上,你可以移动到两边(4和6)。因此,在每一步中,你检查前一长度路径的所有可能的结束方式(即有多少个以角落、侧面、‘垂直’或‘底部’零键结束),然后根据之前规定的生成规则生成新的结束计数。
  • 每个角落的结束会增加一条边和一条垂直线。
  • 每个边的结束会增加2个角落和一条底边。
  • 每个垂直线的结束会增加2个角落。
  • 每个底部的结束会增加2条边。

generating possibilities from previous steps

如果您从“1”键开始,则会有一种可能的角解,每步中您都需要计算前一步的角、边、垂直和底部结束数量,然后应用规则生成下一个计数。
使用普通JavaScript代码。
function paths(n) {
    //Index to 0
    var corners = 1;
    var verticals = 0;
    var bottom = 0;
    var sides = 0;

    if (n <= 0) {
        //No moves possible for paths without length 
        return 0;
    }

    for (var i = 1; i < n; i++) {
        var previousCorners = corners;
        var previousVerticals = verticals;
        var previousBottom = bottom;
        var previousSides = sides;

        sides = 1 * previousCorners + 2 * previousBottom;
        verticals = 1 * previousCorners;
        bottom = 1 * previousSides;
        corners = 2 * previousSides + 2 * previousVerticals;
        //console.log("Moves: %d, Length: %d, Sides: %d, Verticals: %d, Bottom: %d, Corners: %d, Total: %d", i, i + 1, sides, verticals, bottom, corners, sides+verticals+bottom+corners);  
    }

    return sides + verticals + bottom + corners;

}

for (var i = 0; i <= 10; i++) {
    console.log(paths(i));  
}

很好的观察和解决方案。我认为这类似于自底向上的动态规划方法,只需维护前一行即可。 - boni

1

一个更简单的答案。

#include<stdio.h>

int a[10] = {2,2,2,2,3,0,3,2,2,2};
int b[10][3] = {{4,6},{6,8},{7,9},{4,8},{0,3,9},{},{1,7,0},{2,6},{1,3},{2,4}};

int count(int curr,int n)
{
    int sum = 0;
    if(n==10)
        return 1;
    else
    {
        int i = 0;
        int val = 0;
        for(i = 0; i < a[curr]; i++)
        {
            val = count(b[curr][i],n+1);
            sum += val;
        }
        return sum;
    }
}

int main()
{
    int n = 1;
    int val = count(1,0);
    printf("%d\n",val);
}

庆祝!!


n==10 应该是9才对,因为你必须从1开始作为第一个数字,只需要再加上9个数字就可以得到一个10位数。 - Sarp Centel

1
这可以在O(log N)的时间内完成。将键盘和可能的移动视为图G(V,E),其中顶点是可用数字,边表示哪些数字可以跟随哪些数字。现在对于每个输出位置i,我们可以形成一个向量Paths(i),其中包含每个顶点可以到达的不同路径数。现在很容易看出,对于给定的位置i和数字v,它可以通过的可能路径是可以通过可能的前导数字到达的不同路径之和,或者Paths(i)[v] = sum(Paths(i-1)[v2] * (1 if (v,v2) in E else 0) for v2 in V )。现在,这是取前一个向量的每个位置与邻接矩阵的相应列中的位置的乘积之和。因此,我们可以将其简化为Paths(i) = Paths(i-1) · A,其中A是图的邻接矩阵。消除递归并利用矩阵乘法的结合律,这变为Paths(i) = Paths(1) · A^(i-1)。我们知道Paths(1):我们只有一条路径,通向数字1。

一个n位数字的总路径数是每个数字路径的总和,因此最终算法变为:TotalPaths(n) = sum( [1,0,0,0,0,0,0,0,0,0] · A^(n-1) )

通过平方运算可以在O(log(n))时间内计算出指数,如果乘法需要常数时间,则复杂度为O(M(n) * log(n)),其中M(n)是您喜欢的任意精度乘法算法的复杂度,适用于n位数字。


很好地使用了邻接矩阵。但请注意,您将N视为电话号码的长度,而问题的复杂性是根据可用数字的数量来衡量的。因此,计算A的10次方的时间复杂度为O(n^2)。 - Tomer Vromen
1
不,我认为N应该是电话号码的长度。如果数字更多,键盘应该长什么样? - aioobe
对于任意大小的键盘和任意移动,复杂度将基于朴素稀疏矩阵乘法O(dmlog n),其中d是数字数量,m是可能移动的总数(m <= d^2)。基于网格的键盘将具有O(d)个可能的移动,因此如果问题是关于键盘大小的,则该算法将是二次的。 - Ants Aasma
实际上,这个解决方案可以在N的常数时间内运行。由于A是对称的,它可以被对角化,然后取A^N就变成了将10个实数取到N次方,这可以假定为常数时间,并执行一些与N无关的其他操作。 - Nir Friedman

0

我实现了暴力算法和动态规划模型

import queue


def chess_numbers_bf(start, length):
    if length <= 0:
        return 0
    phone = [[7, 5], [6, 8], [3, 7], [9, 2, 8], [], [6, 9, 0], [1, 5], [0, 2], [3, 1], [5, 3]]
    total = 0
    q = queue.Queue()
    q.put((start, 1))

    while not q.empty():
        front = q.get()
        val = front[0]
        len_ = front[1]
        if len_ < length:
            for elm in phone[val]:
                q.put((elm, len_ + 1))
        else:
            total += 1
    return total


def chess_numbers_dp(start, length):
    if length <= 0:
        return 0

    phone = [[7, 5], [6, 8], [3, 7], [9, 2, 8], [], [6, 9, 0], [1, 5], [0, 2], [3, 1], [5, 3]]
    memory = {}

    def __chess_numbers_dp(s, l):
        if (s, l) in memory:
            return memory[(s, l)]
        elif l == length - 1:
            memory[(s, l)] = 1
            return 1
        else:
            total_n_ways = 0
            for number in phone[s]:
                total_n_ways += __chess_numbers_dp(number, l+1)
            memory[(s, l)] = total_n_ways
            return total_n_ways
    return __chess_numbers_dp(start, 0)


# bf
for i in range(0, 10):
    print(i, chess_numbers_bf(3, i))
print('\n')

for i in range(0, 10):
    print(i, chess_numbers_bf(9, i))
print('\n')

# dp
for i in range(0, 10):
    print(i, chess_numbers_dp(3, i))
print('\n')

# dp
for i in range(0, 10):
    print(i, chess_numbers_dp(9, i))
print('\n')

0
//Both the iterative and recursive with memorize shows count as 1424 for 10 digit numbers starting with 1. 
int[][] b = {{4,6},{6,8},{7,9},{4,8},{0,3,9},{},{1,7,0},{2,6},{1,3},{2,4}};
public int countIterative(int digit, int length) {
    int[][] matrix = new int[length][10];
    for(int dig =0; dig <=9; dig++){
          matrix[0][dig] = 1;
    }
    for(int len = 1; len < length; len++){
        for(int dig =0; dig <=9; dig++){
          int sum = 0;
          for(int i : b[dig]){
            sum += matrix[len-1][i];
          }
          matrix[len][dig] = sum;
        }
    }
    return matrix[length-1][digit];
}

public int count(int index, int length, int[][] matrix ){
    int sum = 0;
    if(matrix[length-1][index] > 0){
        System.out.println("getting value from memoize:"+index + "length:"+ length);
        return matrix[length-1][index];
    }
    if( length == 1){
        return 1;
    }
    for(int i: b[index] )  {
         sum += count(i, length-1,matrix);
    }
    matrix[length-1][index] = sum;
    return sum;
}

0

这个问题也可以被建模成一个约束满足问题(简称CSP)。

我建议使用快速且可扩展的Minion求解器,您可以在这里找到它。

建模可能会很繁琐和耗时(学习曲线陡峭)。

与其使用Minion语言输入,我的建议是使用独立于求解器的建模语言,例如ESSENCE,并相应地找到转换器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接