Có cách nào thanh lịch và nhanh chóng để kiểm tra các bit 1 trong số nguyên có nằm trong vùng liền kề không?


85

Tôi cần kiểm tra xem các vị trí (từ 0 đến 31 đối với số nguyên 32 bit) với giá trị bit 1 có tạo thành vùng liền kề hay không. Ví dụ:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Tôi muốn bài kiểm tra này, tức là một số chức năng has_contiguous_one_bits(int), có thể di động được.

Một cách rõ ràng là lặp qua các vị trí để tìm bit đặt đầu tiên, sau đó là bit không đặt đầu tiên và kiểm tra xem có thêm bit nào không.

Tôi tự hỏi liệu có tồn tại một cách nhanh hơn không? Nếu có các phương pháp nhanh chóng để tìm các bit đặt cao nhất và thấp nhất (nhưng từ câu hỏi này, có vẻ như không có bất kỳ phương pháp di động nào), thì một cách triển khai khả thi là

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

Vui vẻ thôi, đây là 100 số nguyên đầu tiên có các bit liền nhau:

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

chúng (tất nhiên) có dạng (1<<m)*(1<<n-1)không phủ định mn.


4
@aafulei vâng, 0x0nhỏ gọn. Sẽ dễ dàng hơn để xác định điều ngược lại (không gọn nhẹ): nếu có các bit đặt wo như vậy thì có ít nhất một bit chưa được đặt giữa chúng.
Walter

1
@KamilCuk h>=ltheo chức năng (ngụ ý) của highest_set_bit()lowest_set_bit()
Walter


6
Liên kết OEIS đó cho biết những con số này có các chữ số không tăng khi ở dạng nhị phân. Một cách khác để đề cập đến chúng sẽ là nói rằng chúng liền kề (hoặc có thể kết nối). Đối với nhà toán học này, "compact" có nghĩa là một cái gì đó rất khác.
Teepeemm

1
@Teepeemm Tôi nghĩ một lý do khiến câu hỏi này trở thành những câu hỏi nóng trên mạng chính xác là do việc sử dụng sai từ compact này, đó chắc chắn là lý do tại sao tôi nhấp vào nó: Tôi đã không suy nghĩ nhiều và tự hỏi làm thế nào để định nghĩa độ gọn theo cách đó. Rõ ràng là nó không có ý nghĩa.
Không ai

Câu trả lời:


147
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

Tóm tắt:

x & -xđưa ra bit thấp nhất được đặt trong x(hoặc bằng 0 nếu xlà 0).

x + (x & -x) chuyển đổi chuỗi số 1 liên tiếp thấp nhất thành 1 chuỗi duy nhất (hoặc kết thúc bằng 0).

x & x + (x & -x) xóa 1 bit đó.

(x & x + (x & -x)) == 0 kiểm tra xem có còn lại bit 1 nào không.

Lâu hơn:

-xbằng ~x+1, sử dụng phần bù của hai, mà chúng tôi giả định. Sau khi các bit được lật vào ~x, thêm 1 mang để nó lật ngược các bit 1 thấp vào ~xvà bit 0 đầu tiên nhưng sau đó dừng lại. Do đó, các bit thấp của -xtối đa và bao gồm 1 đầu tiên của nó giống như các bit thấp của x, nhưng tất cả các bit cao hơn đều bị lật. (Ví dụ: ~10011100cho 01100011và thêm 1 thì cho 01100100, vì vậy giá thấp 100giống nhau, nhưng giá trị cao 10011được chuyển sang 01100.) Sau đó, x & -xcho chúng ta bit duy nhất là 1 trong cả hai, đó là bit thấp nhất 1 ( 00000100). (Nếu xlà 0, x & -xlà 0.)

Thêm điều này để xthực hiện tất cả các 1 liên tiếp, thay đổi chúng thành 0. Nó sẽ để lại giá trị 1 ở bit 0 cao hơn tiếp theo (hoặc chuyển qua mức cao nhất, để lại tổng được bao bọc bằng 0) ( 10100000.)

Khi điều này được ANDed với x, có số 0 ở những vị trí mà số 1 đã được thay đổi thành số 0 (và cũng có thể ở nơi mang đã thay đổi 0 thành 1). Vì vậy, kết quả không chỉ bằng 0 nếu có thêm 1 bit cao hơn.


23
Ít nhất ai đó biết cuốn sách Hacker's Delight. Hãy xem chương 2-1 để biết câu trả lời. Nhưng điều này đã được đọc nhiều lần ở đây trên SO. Dù sao: +1
Armin Montigny

33
Tôi hy vọng nếu bạn đã bao giờ viết mã như vậy trong sản xuất, bạn bao gồm các lời giải thích trong ý kiến;)
Polygnome

14
Điều này mang lại lợi ích tuyệt vời từ BMI1 x86 khi thực hiện x & -xtrong một blsihướng dẫn duy nhất , đó là 1 lần trên Intel, 2 lần trên AMD Zen. godbolt.org/z/5zBx-A . Nhưng không có BMI1, phiên bản của @ KevinZ thậm chí còn hiệu quả hơn.
Peter Cordes

3
@TommyAndersen: _Boollà một từ khóa chuẩn, theo C 2018 6.4.1 1.
Eric Postpischil

1
@Walter: Hmm? Mã này sử dụng unsigned. Nếu bạn muốn thực hiện kiểm tra phần bù có dấu của hai người int, cách dễ nhất là chuyển nó đến quy trình trong câu trả lời này, để cho phép intchuyển đổi thành unsigned. Điều đó sẽ cho kết quả mong muốn. Áp dụng các hoạt động hiển thị cho một ký inttrực tiếp có thể có vấn đề, do các vấn đề tràn / mang. (Nếu bạn muốn kiểm tra phần bổ sung hoặc dấu hiệu và độ lớn intcủa một người, đó là một vấn đề khác, phần lớn chỉ quan tâm đến lý thuyết ngày nay.)
Eric Postpischil

29

Thực ra không cần sử dụng bất kỳ bản chất nào.

Đầu tiên lật tất cả các số 0 trước số 1. Sau đó kiểm tra xem giá trị mới có phải là số mersenne hay không. Trong thuật ngữ này, số không được ánh xạ thành true.

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

Tất nhiên, nếu bạn muốn sử dụng bản chất, đây là phương pháp popcount:

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}

2
Phiên bản đầu tiên giảm chỉ còn 4 hướng dẫn nếu được biên dịch với -mtbm, khai thác blsfill/ blcfillhướng dẫn. Nó sẽ là phiên bản ngắn nhất được đề xuất cho đến nay. Thật không may, hầu như không có bộ xử lý nào hỗ trợ phần mở rộng tập lệnh đó .
Giovanni Cerretani

19

Trên thực tế, bạn không cần phải đếm các số 0 ở đầu. Theo đề xuất của pmg trong các nhận xét, khai thác thực tế rằng các số bạn đang tìm kiếm là các số thuộc dãy OEIS A023758 , tức là Các số có dạng 2 ^ i - 2 ^ j với i> = j , bạn có thể chỉ cần đếm các số không ở cuối ( tức là j - 1 ), chuyển các bit đó thành giá trị ban đầu (tương đương với thêm 2 ^ j - 1 ), rồi kiểm tra xem giá trị đó có dạng 2 ^ i - 1 hay không . Với bản chất GCC / clang,

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

Phiên bản này nhanh hơn một chút so với phiên bản của bạn và phiên bản do KamilCuk đề xuất và phiên bản của Yuri Feldman chỉ với số tiền lẻ.

Nếu bạn đang sử dụng C ++ 20, bạn có thể nhận được một hàm di động bằng cách thay thế __builtin_ctzbằng std::countr_zero:

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

Việc ép kiểu là xấu, nhưng nó đang cảnh báo bạn rằng tốt hơn nên làm việc với các kiểu không dấu khi thao tác với các bit. Các lựa chọn thay thế Pre-C ++ 20 là boost::multiprecision::lsb.

Biên tập:

Điểm chuẩn trên liên kết gạch ngang bị giới hạn bởi thực tế là không có hướng dẫn về số tiền lẻ nào được phát ra cho phiên bản Yuri Feldman. Đang cố gắng biên dịch chúng trên PC của tôi với -march=westmere, tôi đã đo thời gian sau cho 1 tỷ lần lặp với các trình tự giống hệt nhau từ std::mt19937:

  • phiên bản của bạn: 5,7 giây
  • Phiên bản thứ hai của KamilCuk: 4,7 giây
  • phiên bản của tôi: 4,7 giây
  • Phiên bản đầu tiên của Eric Postpischil: 4,3 giây
  • Phiên bản của Yuri Feldman (sử dụng rõ ràng __builtin_popcount): 4,1 giây

Vì vậy, ít nhất là trên kiến ​​trúc của tôi, tốc độ nhanh nhất dường như là thứ có popcount.

Chỉnh sửa 2:

Tôi đã cập nhật điểm chuẩn của mình với phiên bản mới của Eric Postpischil. Như được yêu cầu trong các bình luận, mã của bài kiểm tra của tôi có thể được tìm thấy ở đây . Tôi đã thêm một vòng lặp no-op để ước tính thời gian cần thiết của PRNG. Tôi cũng đã thêm hai phiên bản của KevinZ. Mã đã được biên dịch trên clang với -O3 -msse4 -mbmiđể lấy popcntblsihướng dẫn (nhờ Peter Cordes).

Kết quả: Ít nhất về kiến ​​trúc của tôi, phiên bản của Eric Postpischil chính xác nhanh hơn phiên bản của Yuri Feldman và nhanh hơn ít nhất hai lần so với bất kỳ phiên bản nào khác được đề xuất cho đến nay.


Tôi đã gỡ bỏ một hoạt động: return (x & x + (x & -x)) == 0;.
Eric Postpischil

3
Đây là điểm chuẩn cho một phiên bản cũ hơn của phiên bản @ Eric, phải không? Với phiên bản hiện tại, Eric's biên dịch với ít hướng dẫn gcc -O3 -march=nehalem(để cung cấp popcnt) hoặc ít hơn nếu BMI1 blsikhả dụng cho x & -x: godbolt.org/z/zuyj_f . Và các hướng dẫn đều là một lần đơn giản, ngoại trừ popcntphiên bản của Yuri có độ trễ 3 chu kỳ. (Nhưng tôi cho rằng bạn đang giảm thông lượng.) Tôi cũng cho rằng bạn phải xóa and valkhỏi Yuri's nếu không sẽ chậm hơn.
Peter Cordes

2
Ngoài ra, bạn đã benchmark trên phần cứng nào? Liên kết mã điểm chuẩn đầy đủ của bạn trên Godbolt hoặc thứ gì đó sẽ là một ý tưởng hay, vì vậy người đọc trong tương lai có thể dễ dàng kiểm tra việc triển khai C ++ của họ.
Peter Cordes

2
Bạn cũng nên thử nghiệm phiên bản của @ KevinZ; nó biên dịch thành ít hướng dẫn hơn mà không có BMI1 (ít nhất là với clang; phiên bản không nội tuyến của gcc lãng phí a movvà không tận dụng được lea): godbolt.org/z/5jeQLQ . Với BMI1, phiên bản của Eric vẫn tốt hơn trên x86-64, ít nhất là trên Intel, đó blsilà một uop duy nhất, nhưng đó là 2 uop trên AMD.
Peter Cordes

15

Không chắc chắn về tốc độ nhanh, nhưng có thể thực hiện một lớp lót bằng cách xác minh rằng val^(val>>1)có nhiều nhất 2 bit trên.

Điều này chỉ hoạt động với các kiểu không có dấu: dịch chuyển ở 0trên cùng (dịch chuyển lôgic) là cần thiết, không phải dịch chuyển bên phải số học mà dịch chuyển trong bản sao của bit dấu.

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

Để từ chối 0(nghĩa là chỉ chấp nhận các đầu vào có chính xác 1 nhóm bit liền kề), logic-AND với giá valtrị khác 0. Các câu trả lời khác cho câu hỏi này chấp nhận 0là nhỏ gọn.

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++ có thể hiển thị popcount qua std::bitset::count()hoặc trong C ++ 20 quastd::popcount . C vẫn không có một cách di động nào có thể biên dịch một cách đáng tin cậy thành một cửa sổ bật lên hoặc hướng dẫn tương tự trên các mục tiêu có sẵn.


2
Cũng là nhanh nhất, cho đến nay.
Giovanni Cerretani

2
Tôi nghĩ bạn cần sử dụng kiểu không dấu để đảm bảo bạn chuyển sang số không, không phải bản sao của bit dấu. Hãy cân nhắc 11011111. Số học dịch chuyển sang phải, nó trở thành 11101111, và XOR là 00110000. Với dịch chuyển phải hợp lý (dịch chuyển ở 0trên cùng), bạn nhận được 10110000và phát hiện chính xác nhiều nhóm bit. Chỉnh sửa để khắc phục điều đó.
Peter Cordes

3
Điều này thực sự thông minh. Nhiều khi tôi không thích phong cách này (IMO chỉ sử dụng __builtin_popcount(), mọi trình biên dịch đều có một trình biên dịch sơ khai như vậy ngày nay), đây là nhanh nhất (trên một cpu hiện đại). Trên thực tế, tôi sẽ tranh luận rằng bản trình bày đó rất quan trọng, bởi vì trên một cpu không có POPCNT như một chỉ dẫn duy nhất, việc triển khai của tôi có thể đánh bại điều này. Do đó, nếu bạn định sử dụng triển khai này, bạn chỉ nên sử dụng nội tại. std::bitsetcó một giao diện khủng khiếp.
KevinZ

9

CPU có hướng dẫn dành riêng cho việc đó, rất nhanh. Trên PC, chúng là BSR / BSF (được giới thiệu vào năm 80386 vào năm 1985), trên ARM chúng là CLZ / CTZ

Sử dụng một để tìm chỉ số của bit đặt ít quan trọng nhất, dịch số nguyên sang phải bằng số đó. Sử dụng một số khác để tìm chỉ số của bit tập hợp quan trọng nhất, so sánh số nguyên của bạn với (1u << (bsr + 1)) - 1.

Thật không may, 35 năm không đủ để cập nhật ngôn ngữ C ++ cho phù hợp với phần cứng. Để sử dụng các hướng dẫn này từ C ++, bạn sẽ cần bản chất, những hướng dẫn này không di động và trả về kết quả ở các định dạng hơi khác. Sử dụng bộ tiền xử lý, #ifdefv.v., để phát hiện trình biên dịch và sau đó sử dụng bản chất thích hợp. Trong MSVC họ _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. Trong GCC và tiếng kêu, họ là __builtin_clz__builtin_ctz.


2
@ e2-e4 Visual studio không hỗ trợ lắp ráp nội tuyến khi biên dịch cho AMD64. Đó là lý do tại sao tôi khuyên bạn nên tìm hiểu bản chất.
Soonts

5
Vì C ++ 20 có std::countr_zerostd::countl_zero. Trong trường hợp bạn đang sử dụng Boost, nó có các trình bao bọc di động được gọi là boost::multiprecision::lsbboost::multiprecision::msb.
Giovanni Cerretani

8
Điều này hoàn toàn không trả lời câu hỏi của tôi - Tôi tự hỏi tại sao nó lại nhận được bất kỳ sự ủng hộ nào
Walter

3
@Walter Ý bạn là "không trả lời" là gì? Tôi đã trả lời chính xác những gì bạn nên làm, sử dụng bộ tiền xử lý và sau đó là bản chất.
Soonts

2
Rõ ràng C ++ 20 cuối cùng đã thêm #include <bit> en.cppreference.com/w/cpp/header/bit với bit-scan, popcount và xoay. Thật thảm hại khi phải mất nhiều thời gian như vậy để hiển thị bit-scan một cách linh động, nhưng bây giờ tốt hơn là không bao giờ. (Portable popcnt đã được cung cấp thông qua std::bitset::count().) C ++ 20 vẫn còn thiếu một số thứ mà Rust cung cấp ( doc.rust-lang.org/std/primitive.i32.html ), ví dụ như bit-reverse và endian mà một số CPU cung cấp hiệu quả nhưng không phải tất cả. Một bản dựng sẵn di động cho một hoạt động mà bất kỳ CPU nào cũng có, mặc dù người dùng cần biết điều gì là nhanh.
Peter Cordes

7

So sánh với số không thay vì số không sẽ tiết kiệm một số hoạt động:

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

Các kết quả sau đây ít hơn một hướng dẫn so với hướng dẫn ở trên gcc10 -O3trên x86_64 và sử dụng phần mở rộng về dấu hiệu:

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

Đã thử nghiệm trên chốt đỡ .


Thật không may, điều này không phải là di động. Tôi luôn sợ mình hiểu sai về tính ưu việt của toán tử với những toán tử shift đó - bạn có chắc chắn ~val<<h>>h>>l == 0làm như những gì bạn nghĩ không?
Walter

4
Vâng, tôi chắc chắn, dù sao cũng đã chỉnh sửa và thêm niềng răng. Och, vậy bạn quan tâm đến một giải pháp di động? Bởi vì tôi đã nhìn there exists a faster way?và cho rằng bất cứ điều gì xảy ra.
KamilCuk

5

Bạn có thể diễn đạt lại yêu cầu:

  • đặt N số bit khác với số trước đó (bằng cách lặp qua các bit)
  • nếu N = 2 và và bit đầu tiên hoặc cuối cùng là 0 thì câu trả lời là có
  • nếu N = 1 thì câu trả lời là có (vì tất cả các số 1 nằm ở một phía)
  • nếu N = 0 thì và bất kỳ bit nào là 0 thì bạn không có 1s nào, tùy thuộc vào bạn nếu bạn coi câu trả lời là có hay không
  • bất cứ điều gì khác: câu trả lời là không

Xem qua tất cả các bit có thể trông như thế này:

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

Nhưng điều này chắc chắn có thể được tối ưu hóa (ví dụ: bằng cách hủy bỏ forvòng lặp khi valueđạt đến 0điều đó có nghĩa là không còn bit quan trọng nào có giá trị 1 nữa).


3

Bạn có thể thực hiện chuỗi tính toán này (giả sử vallà đầu vào):

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

để có được một số có tất cả các số không bên dưới số quan trọng nhất được 1lấp đầy bằng các số.

Bạn cũng có thể tính toán y = val & -valđể loại bỏ tất cả ngoại trừ 1 bit quan trọng nhất trong val(ví dụ: 7 & -7 == 112 & -12 == 4).
Cảnh báo: điều này sẽ không thành công val == INT_MIN, vì vậy bạn sẽ phải xử lý trường hợp này riêng, nhưng điều này là ngay lập tức.

Sau đó, sang phải ytheo một vị trí, để thấp hơn một chút so với LSB thực tế valvà thực hiện quy trình tương tự như đối với x:

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

Sau đó x - yhoặc x & ~yhoặc x ^ ytạo mặt nạ bit 'nhỏ gọn' kéo dài toàn bộ chiều dài của val. Chỉ cần so sánh nó để valxem nếu vallà 'nhỏ gọn'.


2

Chúng tôi có thể sử dụng các hướng dẫn nội trang gcc để kiểm tra xem:

Số lượng bit đặt

int __builtin_popcount (unsigned int x)
Trả về số bit 1 trong x.

bằng (a - b):

a : Chỉ số của bit đặt cao nhất (32 - CTZ) (32 vì 32 bit trong số nguyên không dấu).

int __builtin_clz (unsigned int x)
Trả về số lượng bit 0 đứng đầu trong x, bắt đầu từ vị trí bit quan trọng nhất. Nếu x là 0, kết quả là không xác định.

b : Chỉ số của bit đặt thấp nhất (CLZ):

int __builtin_clz (unsigned int x)
Trả về số lượng bit 0 đứng đầu trong x, bắt đầu từ vị trí bit quan trọng nhất. Nếu x là 0, kết quả là không xác định.

Ví dụ nếu n = 0b0001100110; chúng ta sẽ nhận được 4 với popcount nhưng sự khác biệt về chỉ số (a - b) sẽ trả về 6.

bool has_contiguous_one_bits(unsigned n) {
    return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n);
}

cũng có thể được viết là:

bool has_contiguous_one_bits(unsigned n) {
    return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32;
}

Tôi không nghĩ rằng nó thanh lịch hoặc hiệu quả hơn câu trả lời được ủng hộ nhiều nhất hiện tại:

return (x & x + (x & -x)) == 0;

với lắp ráp sau:

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

nhưng nó có lẽ dễ hiểu hơn.


1

Được rồi, đây là một phiên bản lặp lại trên các bit

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

Hai vòng lặp đầu tiên tìm thấy vùng nhỏ gọn đầu tiên. Vòng lặp cuối cùng kiểm tra xem có bất kỳ bit đặt nào khác ngoài vùng đó hay không.

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.