Giải thích sơ đồ mạch


12

Thách thức của bạn là diễn giải một sơ đồ mạch, hoàn thành với các cổng logic.

Cổng logic (bạn thực sự không cần biết những gì chúng làm / là để hoàn thành thử thách này):

  • Và cổng: a
  • hoặc cổng: o
  • cổng nand: A
  • cũng không phải cổng: O
  • cổng xor: x
  • cổng xnor: X
  • không cổng: ~

Mỗi cổng nhưng cái cuối cùng có hai đầu vào. Các đầu vào là từ một .góc trên cùng bên trái và góc dưới bên trái của hình vuông 3 x 3 ở giữa cổng. Đối với không, đầu vào là trực tiếp bên trái của nó. Đầu ra là một. bên phải trực tiếp.

Dây được đại diện bởi -|\/.=

  • - tiếp xúc hai dây, một bên phải và một bên trái: c-c
  • | tiếp xúc hai dây, một ở trên và một bên dưới:

    c
    |
    c
    
  • /\làm việc như sau:

    c        c
     \      /
      c    c
    
  • . liên lạc với mọi dây xung quanh:

    ccc
    c.c
    ccc
    
  • =đặc biệt; nó kết nối các dây liền kề trên nó:

    -=-
    

    nối hai dây. Trong những điều sau đây

    \|/
    -=-
    /|\
    

    mỗi dây đối diện được kết nối với nhau, nhưng không phải là dây khác (đây là nơi khác với nó .).

  • Để dòng điện chạy qua, cả hai dây phải được kết nối với dây còn lại, vì vậy |-, dòng điện không chảy.

Ví dụ về hệ thống dây điện:

      .-.
     =   \
 .--. .---=---
-.   =     .--
 .--. .-------

đầu vào được chia thành hai dây và sau đó chia thành ba. Trong phần tách này, dây dưới cùng di chuyển đến giữa và phần dưới của dây trên cùng xuất hiện ở phía dưới. Tiếp theo, đầu của ba dây được di chuyển đến giữa.

Mẫu dây có cổng:

--.
   o..~..
--.      o.---
   a.---.
--.

Định dạng đầu vào:

  • mỗi dây đầu vào sẽ được dán nhãn bằng một chữ số. Ở cuối (ngay trước dòng mới), mỗi đầu ra sẽ được gắn nhãn :(và dây sẽ luôn đi thẳng vào nó, tức là -:hoặc .:hoặc=: )
  • đầu vào sẽ luôn luôn hợp lệ; sẽ không có các vòng hoặc dây nối với nhau mà không có cổng. Lưu ý rằng có thể có dây với đầu lỏng.
  • = sẽ chỉ được sử dụng khi cần thiết

Định dạng đầu ra:

  • mỗi đầu vào được tham chiếu với số lượng tương ứng.
  • một biểu thức được xuất ra. Ví dụ: nếu dây tính toán đầu vào 1 và đầu vào 2, đầu ra là1a2 .
  • bất cứ chức năng nào được xuất ra đều phải tương ứng với các cổng logic ở đầu.
  • để hiển thị không, đặt ~trước đúng nơi.
  • đối với nhiều hàm, sử dụng dấu ngoặc đơn để hiển thị thứ tự thực hiện. Dấu ngoặc đơn cũng có thể được sử dụng khi chỉ có một chức năng duy nhất. Ví dụ,

    1-.~.-.
           A.~.-:
          .
    2-.  /
       x.
    3-.
    

    có một đầu ra có thể là ~((2x3)A(~1))

  • nhiều đầu ra phải được phân tách bằng một dòng mới (hoặc tương đương)

Đầu vào mẫu:

1--.---.
    x.  \
2--.  x.-=---------.
     .    .~.       X.--.
3---. .      a.----.   /
       O.~.-.       \ /
      .              =
4-.  /              / .-:
   X.              .----:
5-.

Một đầu ra tương ứng có thể:

(~1)a(~(3O(4X5))
((1x2)x3)X((~1)a(~(3O(4X5))))

Oooohhhh, thú vị! Tôi sẽ cho nó một viên đạn.
cjfaure

5
Tôi dự đoán một exolang thú vị xuất phát từ điều này, nếu chúng ta mở rộng nó theo cách để làm cho nó hoàn chỉnh.
Victor Stafusa

Trong trường hợp "lỗi trình biên dịch" (tức là nối dây đầu vào không đúng định dạng) thì trình thông dịch phải làm gì?
Victor Stafusa

Và, nếu tôi kết nối trực tiếp hai đầu vào? Hoặc kết nối trực tiếp hai đầu ra? Hoặc đầu vào dòng mở đến đầu ra?
Victor Stafusa

1
@Victor Điều này đã tương tự. Nhưng tôi đã đi trước và tạo ra một cái khác
Justin

Câu trả lời:


4

Trăn 2488 1567 806 706 697 657 653

Yay cho gzip + exec!

import zlib,base64;exec zlib.decompress(base64.b64decode('eNp1U8FuqzAQvPMV7sm2gBSuuFupX9BLD5UoBxNMMAkEgQmJVPXb364Daiu9ntaznt2dWYzthvPo2HSbgsrU7E3so0FmAWtgnyeFshjSImC2Zs1Tws4js/fQPMPJ9KKTlFrPeVPIbDRuHnvOA3YByuS2UCNwrloYqOMRQ1ooDY0qwaoKRJxGSZRKP+QCwBn/0YRyzPYcYq77irUATVbGcIytGkN4E7mOyiLayx/MT888AthMx9DGDTLj/zIfPz44emUGqC/Zoio1UdFzohzFp0TNNA7xQhFxDWJiNGNG98L54yLVYUsv3+kZx9G8/uyEoQFk8NELrDeIIggf5Cb3b3/I3nnFNdZe0QOrCHl4+4ZsgVyH16gMb4XHq4IrwA0gkV7kAwyZH7Fs7f0S/O7IbnZX7jelzy+v13f8LsAFD0kVfrQyTklZyCUPL+F2Ef66WHug7i9f/bWyfnOIsrNTZQ/WCXxCcAnY/QmwMeggLwIyeCKD+FB3k6tsj/K6nR4G01fiZCcnTlIGBkw/d2bUzvgSG2kqMvhOkU+ZNirvGS1XgyWKy/xS2TDa3uE/kNuoJX0UC/kP8j/kmA=='))

Hạn chế và giả định

Vì vậy, chỉ có tối đa 9 đầu vào được hỗ trợ - nhiều chữ số không được xử lý chính xác. Vì thông số kỹ thuật chỉ ra rằng các đầu vào được dán nhãn bằng một chữ số , không phải số , điều này được cho phép.


Đầu vào và đầu ra

Đầu vào được thực hiện thông qua tiêu chuẩn trong, và đầu ra là thông qua tiêu chuẩn ra.


Kiểm tra

Mẫu đầu vào và đầu ra:

1--.---.
    x.  \
2--.  x.-=---------.
     .    .~.       X.--.
3---. .      a.----.   /
       O.~.-.       \ /
      .              =
4-.  /              / .-:
   X.              .----:
5-.


(~(1))a(~((3)O((4)X(5))))
(((1)x(2))x(3))X((~(1))a(~((3)O((4)X(5)))))

Đã thử nghiệm tại đây: http://ideone.com/gP4CIq


Thuật toán

Về cơ bản, nó là một DFS khá ngây thơ từ các đầu ra. Đối với mỗi đầu ra, nó bắt đầu từ ký tự một bên trái và theo dõi dây, phân nhánh (và thêm vào biểu thức) ở mỗi cổng. Khi nó đạt đến một đầu vào, nó thêm nó vào biểu thức và quay lại điểm cuối cùng mà nó phân nhánh, vì chúng ta có thể chắc chắn rằng việc phân nhánh là không thể nếu không có cổng. Và tất nhiên bất kỳ trường hợp không hợp lệ đều bị loại bỏ. Không có gì thực sự đặc biệt với nó - và do đó nó có thể dài hơn nó có thể.


Ghi chú

Kích thước có thể giảm một chút công bằng với một số tái cấu trúc, nhưng tôi đã dành đủ thời gian cho việc này cho ngày hôm nay. Các phiên bản golf thủ công là một nén.

Việc nén gzip làm cho việc chơi golf trở nên thú vị, bởi vì bộ nhớ đệm nhất định (ví dụ d=(-1,0,1)) thực sự chiếm nhiều không gian hơn là để thuật toán nén chăm sóc nó. Tuy nhiên, tôi đã chọn chơi golf phiên bản thủ công càng nhiều càng tốt thay vì tối ưu hóa để nén.


Chơi gôn thủ công ( 909 895 840 803):

import sys
def T(c,p):
 h=c[0];i=c[1]
 if h<0 or i<0 or h>=len(m)or i>=len(m[h]):return''
 v=m[h][i];r='';j=p[0];k=p[1];a=h;b=i;d=(-1,0,1)
 if v==' ':return''
 if v in'=-'and j==h:b-=k-i;r+=T([a,b],c)
 if v in'=|'and k==i:a-=j-h;r+-T([a,b],c)
 if v in'=/\\':
  e=j==h or k==i;s=j-h>0;t=j-h<0;u=k-i>0;w=k-i<0;f=(s and u)or(t and w);g=(s and w)or(t and u)
  if not(e or v=='/'and f or v=='\\'and g):a-=j-h;b-=k-i;r+=T([a,b],c)
 if v=='.':
  for x in d:
   for y in d:
    w=[a+x,b+y]
    if not(x==y==0)and w!=p:r+=T(w,c)
 if j==h and k-i>0:
  if v in'aoAOxX':r='('+T([a-1,b-1],c)+')'+v+'('+T([a+1,b-1],c)+')'
  if v=='~':r='~('+T([a,b-1],c)+')'
 if v.isdigit():r=v
 return r
m=[]
for l in sys.stdin:
 m.append(list(l))
e=enumerate
for i,a in e(m):
 for j,b in e(a):
  if b==':':
   print T([i,j-1],[i,j])

Toàn vô dụng (2488):

import sys

def findOuts(c):
    for i, iVal in enumerate(c):
        for j, jVal in enumerate(iVal):
            if jVal == ':':
                yield [i, j]

def trace(pos, prev):
    if pos[0] < 0 or pos[1] < 0 or pos[0] >= len(circuit) or pos[1] >= len(circuit[pos[0]]):
        return ''
    val = circuit[pos[0]][pos[1]]
    if val == ' ':
        return ''
    next = pos[:]
    ret = ''
    if val in '=-':
        if prev[0] == pos[0]:
            next[1] -= prev[1] - pos[1]
            ret += trace(next, pos)
    if val in '=|':
        if prev[1] == pos[1]:
            next[0] -= prev[0] - pos[0]
            ret += trace(next, pos)
    if val in '=/\\':
        # top-bottom, left-right
        tblr = prev[0] == pos[0] or prev[1] == pos[1]
        # top-left, bottom-right
        tlbr = (prev[0] - pos[0] == 1 and prev[1] - pos[1] == 1) or (prev[0] - pos[0] == -1 and prev[1] - pos[1] == -1)
        # top-right, bottom-left
        trbl = (prev[0] - pos[0] == 1 and prev[1] - pos[1] == -1) or (prev[0] - pos[0] == -1 and prev[1] - pos[1] == 1)
        if not ((val == '/' and (tlbr or tblr)) or (val == '\\' and (trbl or tblr)) or (val == '=' and tblr)):
            next[0] -= prev[0] - pos[0]
            next[1] -= prev[1] - pos[1]
            ret += trace(next, pos)

    if val == '.':
        for x in (-1,0,1):
            for y in (-1,0,1):
                if x == y == 0:
                    continue

                w = [next[0] + x, next[1] + y]
                if w == prev:
                    continue

                # only one of them should return anything
                ret += trace(w, pos)

    # assumption that a logic gate always has a . on its connections, as according to spec
    if val in 'aoAOxX':
        # only from the right/output
        if not (prev[0] == pos[0] and prev[1] == pos[1] + 1):
            return ret
        ret = '(' + trace([next[0] - 1, next[1] - 1], pos) + ')' + val + '(' + trace([next[0] + 1, next[1] - 1], pos) + ')'

    if val == '~':
        # only from the right/output
        if not (prev[0] == pos[0] and prev[1] == pos[1] + 1):
            return ret
        ret = '~(' + trace([next[0], next[1] - 1], pos) + ')'

    if val in '123456789':
        ret = val

    return ret

circuit = []
for line in sys.stdin.readlines():
    # padding added to prevent index out of bounds later
    circuit.append(list(line))

for out in findOuts(circuit):
    next = out[:]
    next[1] -= 1
    print trace(next, out)

DFS là gì? Ngoài ra, làm việc ngược từ đầu ra là chính xác những gì tôi đã nghĩ đến.
Justin

@Quincunx Độ sâu tìm kiếm đầu tiên. Về cơ bản, đệ quy (hoặc nói cách khác là sử dụng cấu trúc LIFO, ngăn xếp) và di chuyển càng xa càng tốt dọc theo một con đường cho đến khi nó đi vào ngõ cụt hoặc mục tiêu, tại đó nó quay trở lại điểm phân kỳ cuối cùng và thử các đường khác.
Bob

Giả định tốt về đầu vào. Đó chính xác là những gì tôi muốn nói (và tôi đã cố gắng diễn đạt nó để gợi ý điều đó). Tuy nhiên, chương trình của bạn có hoạt động 0như một chữ số không? Làm thế nào về việc hoán đổi thứ tự để 2đến trước 1, vv
Justin

@Quincunx Tôi đang sử dụng Python .isdigit(), tương đương với regex [0-9]theo như tôi có thể nói. Điều đó có đúng theo thông số kỹ thuật của bạn không? Bạn có ý nghĩa gì khi hoán đổi thứ tự? Cách thức triển khai, nó sẽ đi vào nhánh đi lên của bất kỳ cổng logic nào trước, nhưng không có gì đảm bảo cho việc đặt hàng đầu vào.
Bob

isdigit()là phù hợp. Thứ tự hoán đổi có nghĩa là một cái gì đó giống 2như đầu vào đầu tiên và 1như đầu vào thứ hai (được sắp xếp theo chiều dọc).
Justin

6

Java: 1523 1512 ký tự

import java.util.*;class W{int v=99;Map<Integer,String>t;boolean k;public static void main(String[]y){new W().d();}W(){try{java.io.InputStream i=new java.io.File("r").toURL().openStream();t=new HashMap<>();int a=0,x=0,y=0;while((a=i.read())>-1){if(a==10){y++;x=0;continue;}q(x,y,(a>47&a<58?"!":"")+(char)a);x++;}}catch(Exception e){}}void d(){while(!k){k=!k;for(Map.Entry<Integer,String>g:t.entrySet())e(g.getKey(),g.getValue());}for(String b:t.values())if(b.startsWith("$"))System.out.println(b.substring(1));}void e(int a,String s){if(s==null||!s.startsWith("!"))return;int x=a/v,y=a%v;s=s.substring(1);b(s,x,y,x-1,y+1);b(s,x,y,x,y+1);b(s,x,y,x+1,y+1);b(s,x,y,x-1,y);b(s,x,y,x+1,y);b(s,x,y,x-1,y-1);b(s,x,y,x,y-1);b(s,x,y,x+1,y-1);}void b(String p,int m,int n,int x,int y){String s=t.get(x*v+y);if(s==null)return;boolean g=y==n+1;boolean h=y==n-1;boolean i=x==m+1;boolean j=x==m-1;if(z(s,"-=")&n==y){if(i)b(p,x,y,x+1,y);if(j)b(p,x,y,x-1,y);}if(z(s,"|=")&m==x){if(g)b(p,x,y,x,y+1);if(h)b(p,x,y,x,y-1);}if(z(s,"/=")){if(j&g)b(p,x,y,x-1,y+1);if(i&h)b(p,x,y,x+1,y-1);}if(z(s,"\\=")){if(i&g)b(p,x,y,x+1,y+1);if(j&h)b(p,x,y,x-1,y-1);}if(z(s,".")){q(x,y,"!"+p);u();}if(z(s,"~")){q(x,y,"!~("+p+")");u();}if((s.charAt(0)=='%'&n==y-1)|(s.charAt(0)=='&'&n==y+1)){q(x,y,"!("+p+")"+s.charAt(1)+"("+s.substring(2)+")");u();}if(z(s,"OoAaXx")){q(x,y,(n==y+1?"%":"&")+s+p);u();}if(z(s,":")){q(x,y,"$"+p);u();}}void q(int x,int y,String z){t.put(x*v+y,z);}void u(){k=false;}boolean z(String s,String c){return c.indexOf(s)>-1;}}

Nó cung cấp đầu ra này cho đầu vào mẫu:

(~(((5)X(4))O(3)))a(~(1))
((~(((5)X(4))O(3)))a(~(1)))X(((2)x(1))x(3))

Để ép kích thước của nó:

  • Nó không thực hiện bất kỳ kiểm tra lỗi, xử lý lỗi hoặc xác nhận đầu vào, giả sử rằng đầu vào luôn hợp lệ.
  • Được giới hạn ở 99 dòng đầu vào.
  • Tệp đầu vào của nó phải được gọi chỉ r, không có bất kỳ phần mở rộng tệp nào trong tên.
  • Nó không thực hiện bất kỳ nỗ lực nào để phát hiện nếu dấu ngoặc đơn là hoặc không cần thiết. Nó giả định rằng chúng luôn luôn cần thiết và vì giả định này là sai, nên có nhiều dấu ngoặc đơn hơn mức cần thiết, nhưng vì điều này không làm cho nó thất bại trong thông số kỹ thuật, nên không có vấn đề gì.
  • Thứ tự của các tham số cho mỗi toán tử nhị phân nói chung là không thể đoán trước, vì nó phụ thuộc vào vận tốc mà các giá trị được truyền và từ thứ tự quét tế bào. Nhưng vì tất cả các toán tử nhị phân là giao hoán, nên điều này không có vấn đề gì.

Tôi chắc chắn rằng nó sẽ có thể giảm nó nhiều hơn, nhưng chỉ một chút.

Trình thông dịch được thực hiện dưới dạng một số loại automata di động. Nó quét toàn bộ giá trị cài đặt trường, lặp lại nhiều lần nếu cần cho đến khi không có thay đổi nào được phát hiện.

Đây là một phiên bản chưa được chỉnh sửa:

import java.util.*;

class Wiring {

    int maxLines = 99;
    Map<Integer, String> circuitState;
    boolean finished;

    public static void main(String[] args) {
        new Wiring().interpret();
    }

    Wiring() {

        try {
            // Always read the input from the "r" file, and do not check if it even
            // exists. BTW, the toURL() method is deprecated, but we don't care about
            // this in code-golfing.
            java.io.InputStream stream = new java.io.File("r").toURL().openStream();

            circuitState = new HashMap<>();
            int byteRead = 0, cellX = 0, cellY = 0;

            while ((byteRead = stream.read()) > -1) {

                // Check for line break;
                if (byteRead == 10) {
                    cellY++;
                    cellX = 0;
                    continue;
                }

                // Populate the circuit cell. Precede numbers with an exclamation mark.
                setCircuitCell(cellX, cellY, (byteRead >= '0' & byteRead <= '9' ? "!" : "") + (char) byteRead);
                cellX++;
        } catch (Exception e) {
        }
    }

    void interpret() {
        while (!finished) {
            finished = !finished; // i.e. finished = false;
            for (Map.Entry<Integer, String> entry : circuitState.entrySet()) {
                analyzeCell(entry.getKey(), entry.getValue());
            }
        }

        // Now print the output. To do that scan for cells marked with "$".
        for (String cell : circuitState.values()) {
            if (cell.startsWith("$")) System.out.println(cell.substring(1));
        }
    }

    void analyzeCell(int cellIndex, String cellValue) {
        // Only the cells with a value marked with "!" are worth to analyze.
        if (cellValue == null || !cellValue.startsWith("!")) return;

        // Convert the cellIndex to a bidimensional coordinate.
        int x = cellIndex / maxLines, y = cellIndex % maxLines;

        // Remove the "!".
        cellValue = cellValue.substring(1);

        // Propagate the cell value to neighbouring cells.
        propagateCellData(cellValue, x, y, x - 1, y + 1);
        propagateCellData(cellValue, x, y, x, y + 1);
        propagateCellData(cellValue, x, y, x + 1, y + 1);
        propagateCellData(cellValue, x, y, x - 1, y);
        propagateCellData(cellValue, x, y, x + 1, y);
        propagateCellData(cellValue, x, y, x - 1, y - 1);
        propagateCellData(cellValue, x, y, x, y - 1);
        propagateCellData(cellValue, x, y, x + 1, y - 1);
    }

    void propagateCellData(String cellValue, int sourceX, int sourceY, int targetX, int targetY) {
        String targetContent = circuitState.get(targetX * maxLines + targetY);

        // If the target cell does not exist, just ignore.
        if (targetContent == null) return;

        boolean targetBelowSource = targetY == sourceY + 1;
        boolean targetAboveSource = targetY == sourceY - 1;
        boolean targetRightToSource = targetX == sourceX + 1;
        boolean targetLeftToSource = targetX == sourceX - 1;

        // Propagate horizontally through wires.
        if (isStringContained(targetContent, "-=") & sourceY == targetY) {
            if (targetRightToSource) propagateCellData(cellValue, targetX, targetY, targetX + 1, targetY);
            if (targetLeftToSource) propagateCellData(cellValue, targetX, targetY, targetX - 1, targetY);
        }

        // Propagate vertically.
        if (isStringContained(targetContent, "|=") & sourceX == targetX) {
            if (targetBelowSource) propagateCellData(cellValue, targetX, targetY, targetX, targetY + 1);
            if (targetAboveSource) propagateCellData(cellValue, targetX, targetY, targetX, targetY - 1);
        }

        // Propagate in the diagonal x=-y.
        if (isStringContained(targetContent, "/=")) {
            if (targetLeftToSource & targetBelowSource) {
                propagateCellData(cellValue, targetX, targetY, targetX - 1, targetY + 1);
            }
            if (targetRightToSource & targetAboveSource) {
                propagateCellData(cellValue, targetX, targetY, targetX + 1, targetY - 1);
            }
        }

        // Propagate in the diagonal x=y.
        if (isStringContained(targetContent, "\\=")) {
            if (targetRightToSource & targetBelowSource) {
                propagateCellData(cellValue, targetX, targetY, targetX + 1, targetY + 1);
            }
            if (targetLeftToSource & targetAboveSource) {
                propagateCellData(cellValue, targetX, targetY, targetX - 1, targetY - 1);
            }
        }

        // If we got a dot, store the value there.
        // Do not forget to mark it with "!", so we can rescan it later.
        if (isStringContained(targetContent, ".")) {
            setCircuitCell(targetX, targetY, "!" + cellValue);
            markThatStateChanged();
        }

        // If we got a "~", store the inverted value there.
        // Do not forget to mark it with "!", so we can rescan it later.
        if (isStringContained(targetContent, "~")) {
            setCircuitCell(targetX, targetY, "!~(" + cellValue + ")");
            markThatStateChanged();
        }

        // If we found a binary logical port with one of the values set and
        // we can set the another value, do it. Use "%" and "&" to know which
        // one was already defined.
        // BTW, do not forget to mark it with "!", so we can rescan it later.
        if ((targetContent.charAt(0) == '%' & sourceY == targetY - 1)
                | (targetContent.charAt(0) == '&' & sourceY == targetY + 1))
        {
            setCircuitCell(targetX, targetY,
                    "!(" + cellValue + ")"
                    + targetContent.charAt(1)
                    + "(" + targetContent.substring(2) + ")");
            markThatStateChanged();
        }

        // Found a binary logical port without any value setted, so set it.
        // Use "%" and "&" to mark which one was setted.
        if (isStringContained(targetContent, "OoAaXx")) {
            setCircuitCell(targetX, targetY, (sourceY == targetY + 1 ? "%" : "&") + targetContent + cellValue);
            markThatStateChanged();
        }

        // If we found an output, store the value there.
        // Mark it with "$", so we will print it in the future.
        if (isStringContained(targetContent, ":")) {
            setCircuitCell(targetX, targetY, "$" + cellValue);
            markThatStateChanged();
        }
    }

    void setCircuitCell(int cellX, int cellY, String cellContents) {
        circuitState.put(cellX * maxLines + cellY, cellContents);
    }

    void markThatStateChanged() {
        finished = false;
    }

    boolean isStringContained(String searchingString, String searchTarget) {
        return searchTarget.indexOf(searchingString) > -1;
    }
}

Tiny bit rẻ hơn để sử dụng try{}catch(Exception e){}chứ không phải hai throws Exception. Có lẽ có những thứ khác, nhưng tôi không biết chơi golf Java.
Bob

@Bob Cảm ơn, đề xuất của bạn đã khiến tôi giảm 7 ký tự. Ngoài ra, tôi có thể giảm thêm 4.
Victor Stafusa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.