Perl + Math :: {ModInt, Đa thức, Prime :: Util}, điểm 92819
$m=Math::Polynomial;sub l{($n,$b,$d)=@_;$n||$d||return;$n%$b,l($n/$b,$b,$d&&$d-1)}sub g{$p=$m->interpolate([grep ref$_[$_],0..$map{$p->evaluate($_)}0..$}sub p{prev_prime(128**$s)}sub e{($_,$r)=@_;length||return'';$s=$r+1;s/^[␀␁]/␁$&/;@l=map{mod($_,p$s)}l(Math::BigInt->from_bytes($_),p$s);$@l+$r>p($s)&&return e($_,$s);$a=0;join'',map{map{chr$_+$a}l($_->residue,128,$s,($a^=128))}g(@l)}sub d{@l=split/([␀-␡]+)/,$_[0];@l||return'';$s=vecmax map length,@l;@l=g map{length==$s&&mod($m->new(map{ord()%128}split//)->evaluate(128),p$s)}@l;$$_=$m->new(map{$_->residue}@l)->evaluate(p$s)->to_bytes;s/^␁//;$_}
Hình ảnh điều khiển được sử dụng để thể hiện ký tự điều khiển tương ứng (ví dụ: ␀
ký tự NUL theo nghĩa đen). Đừng lo lắng nhiều về việc cố gắng đọc mã; có một phiên bản dễ đọc hơn dưới đây.
Chạy với -Mbigint -MMath::ModInt=mod -MMath::Polynomial -MNtheory=:all
. -MMath::Bigint=lib,GMP
là không cần thiết (và do đó không được bao gồm trong điểm số), nhưng nếu bạn thêm nó trước các thư viện khác, nó sẽ làm cho chương trình chạy nhanh hơn một chút.
Tính điểm
Thuật toán ở đây có phần dễ ứng dụng, nhưng sẽ khó viết hơn (do Perl không có các thư viện phù hợp). Bởi vì điều này, tôi đã thực hiện một vài sự đánh đổi kích thước / hiệu quả trong mã, trên cơ sở cho rằng các byte có thể được lưu trong mã hóa, không có lý do gì để cố gắng loại bỏ mọi điểm khi chơi golf.
Chương trình bao gồm 600 byte mã, cộng với 78 byte hình phạt cho các tùy chọn dòng lệnh, đưa ra mức phạt 678 điểm. Phần còn lại của điểm được tính bằng cách chạy chương trình trên chuỗi trường hợp tốt nhất và trường hợp xấu nhất (về độ dài đầu ra) cho mọi độ dài từ 0 đến 99 và mọi mức độ bức xạ từ 0 đến 9; trường hợp trung bình là một nơi nào đó ở giữa, và điều này mang lại giới hạn về điểm số. (Không đáng để cố gắng tính giá trị chính xác trừ khi có mục khác có điểm tương tự.)
Do đó, điều này có nghĩa là điểm số từ hiệu quả mã hóa nằm trong phạm vi bao gồm 91100 đến 92141, do đó, điểm số cuối cùng là:
91100 + 600 + 78 = 91778 ≤ điểm 92819 = 92141 + 600 + 78
Phiên bản ít chơi hơn, với ý kiến và mã kiểm tra
Đây là chương trình ban đầu + dòng mới, thụt lề và bình luận. (Trên thực tế, phiên bản chơi gôn được sản xuất bằng cách xóa dòng mới / thụt lề / nhận xét khỏi phiên bản này.)
use 5.010; # -M5.010; free
use Math::BigInt lib=>'GMP'; # not necessary, but makes things much faster
use bigint; # -Mbigint
use Math::ModInt 'mod'; # -MMath::ModInt=mod
use Math::Polynomial; # -MMath::Polynomial
use ntheory ':all'; # -Mntheory=:all
use warnings; # for testing; clearly not necessary
### Start of program
$m=Math::Polynomial; # store the module in a variable for golfiness
sub l{ # express a number $n in base $b with at least $d digits, LSdigit first
# Note: we can't use a builtin for this because the builtins I'm aware of
# assume that $b fits into an integer, which is not necessarily the case.
($n,$b,$d)=@_;
$n||$d||return;
$n%$b,l($n/$b,$b,$d&&$d-1)
}
sub g{ # replaces garbled blocks in the input with their actual values
# The basic idea here is to interpolate a polynomial through all the blocks,
# of the lowest possible degree. Unknown blocks then get the value that the
# polynomial evaluates to. (This is a special case of Reed-Solomon coding.)
# Clearly, if we have at least as many ungarbled blocks as we did original
# elements, we'll get the same polynomial, thus we can always reconstruct
# the input.
# Note (because it's confusing): @_ is the input, $_ is the current element
# in a loop, but @_ is written as $_ when using the [ or # operator (e.g.
# $_[0] is the first element of @_.
# We waste a few bytes of source for efficiency, storing the polynomial
# in a variable rather than recalculating it each time.
$p=$m->interpolate([grep ref$_[$_],0..$#_],[grep ref,@_]);
# Then we just evaluate the polynomial for each element of the input.
map{$p->evaluate($_)}0..$#_
}
sub p{ # determines maximum value of a block, given (radiation+1)
# We split the input up into blocks. Each block has a prime number of
# possibilities, and is stored using the top 7 bits of (radiation+1)
# consecutive bytes of the output. Work out the largest possible prime that
# satisfies this property.
prev_prime(128**$s)
}
sub e{ # encoder; arguments: input (bytestring), radiation (integer)
($_,$r)=@_; # Read the arguments into variables, $_ and $r respectively
length||return''; # special case for empty string
$s=$r+1; # Also store radiation+1; we use it a lot
# Ensure that the input doesn't start with NUL, via prepending SOH to it if
# it starts with NUL or SOH. This means that it can be converted to a number
# and back, roundtripping correctly.
s/^[␀␁]/␁$&/; #/# <- unconfuse Stack Exchange's syntax highlighting
# Convert the input to a bignum, then to digits in base p$s, to split it
# into blocks.
@l=map{mod($_,p$s)}l(Math::BigInt->from_bytes($_),p$s);
# Encoding can reuse code from decoding; we append $r "garbled blocks" to
# the blocks representing the input, and run the decoder, to figure out what
# values they should have.
$#l+=$r;
# Our degarbling algorithm can only handle at most p$s blocks in total. If
# that isn't the case, try a higher $r (which will cause a huge increase in
# $b and a reduction in @l).
@l+$r>p($s)&&return e($_,$s);
# Convert each block to a sequence of $s digits in base 128, adding 128 to
# alternating blocks; this way, deleting up to $r (i.e. less than $s) bytes
# will preserve the boundaries between each block; then convert that to a
# string
$a=0; # we must initialize $a to make this function deterministic
join'',map{map{chr$_+$a}l($_->residue,128,$s,($a^=128))}g(@l)
}
sub d{ # decoder: arguments; encdng (bytestring)
# Reconstruct the original blocks by looking at their top bits
@l=split/([␀-␡]+)/,$_[0];
@l||return''; # special case for empty string
# The length of the longest block is the radiation parameter plus 1 (i.e.
# $s). Use that to reconstruct the value of $s.
$s=vecmax map length,@l;
# Convert each block to a number, or to undef if it has the wrong length.
# Then work out the values for the undefs.
@l=g map{
# Convert blocks with the wrong length to undef.
length==$s&&
# Convert other blocks to numbers, via removing any +128 and then
# using Math::Polynomial to convert the digit list to a number.
mod($m->new(map{ord()%128}split// #/# <- fix syntax highlighting
)->evaluate(128),p$s)
}@l;
# Remove the redundant elements at the end; now that they've reconstructed
# the garbled elements they have no further use.
$#l-=$s-1;
# Convert @l to a single number (reversing the conversion into blocks.)
$_=$m->new(map{$_->residue}@l)->evaluate(p$s)
# Convert that number into a string.
->to_bytes;
# Delete a leading SOH.
s/^␁//; #/# <- unconfuse Stack Exchange's syntax highlighting
# Finally, return the string.
$_
}
### Testing code
use Encode qw/encode decode/;
# Express a string using control pictures + IBM437, to make binary strings
# easier for a human to parse
sub format_string {
($_)=@_;
$_ = decode("Latin-1", $_);
s/[\0-\x1f]/chr (0x2400 + ord $&)/eag;
s/\x7f/chr 0x2421/eag;
s/[ -~\x80-\xff]/decode("IBM437",$&)/eag;
encode("UTF-8","\x{ff62}$_\x{ff63}")
}
sub test {
my ($string, $radiation, $samples) = @_;
say "Input: ", format_string($string);
my $encoding = e($string, $radiation);
say "Encoding: ", format_string($encoding);
say "Input length ", length($string), ", encoding length ", length($encoding), ", radiation $radiation";
my $decoding = d($encoding);
$decoding eq $string or die "Mistake in output!";
say "Decoding: ", format_string($decoding), " from ",
format_string($encoding);
# Pseudo-randomly generate $samples radiation-damaged versions.
srand 1;
for my $i (1..$samples) {
my $encdng = $encoding;
for my $r (1..$radiation) {
substr $encdng, int(rand(length $encdng)), 1, "";
}
my $newdecoding = d($encdng);
say "Decoding: ", format_string($newdecoding), " from ",
format_string($encdng);
$newdecoding eq $string or die "Mistake in output!";
}
say "";
length $encoding;
}
test "abcdefghijklm", 1, 10;
test "abcdefghijklm", 2, 10;
test "abcdefghijklm", 5, 10;
test "abcdefghijklm", 10, 10;
test "\0\0\0\0\0", 1, 10;
test "\5\4\3\2\1", 2, 10;
test "a", 10, 10;
my %minlength = ();
my %maxlength = ();
for my $length (0..99) {
my ($min, $max) = ("", "");
$length and ($min, $max) =
("\2" . "\0" x ($length - 1), "\1" . "\377" x ($length - 1));
for my $radiation (0..9) {
$minlength{"$length-$radiation"} = test $min, $radiation, 1;
$maxlength{"$length-$radiation"} = test $max, $radiation, 1;
}
}
say "Minimum score: ", vecsum values %minlength;
say "Maximum score: ", vecsum values %maxlength;
Thuật toán
Đơn giản hóa vấn đề
Ý tưởng cơ bản là giảm vấn đề "mã hóa xóa" này (không phải là vấn đề được khám phá rộng rãi) thành một vấn đề mã hóa xóa (một lĩnh vực toán học được khám phá toàn diện). Ý tưởng đằng sau mã hóa xóa là bạn đang chuẩn bị dữ liệu được gửi qua "kênh xóa", một kênh đôi khi thay thế các ký tự mà nó gửi bằng ký tự "bị cắt xén" chỉ ra vị trí đã biết của lỗi. (Nói cách khác, nó luôn luôn rõ ràng nơi tham nhũng đã xảy ra, mặc dù nhân vật ban đầu vẫn chưa được biết.) Ý tưởng đằng sau khá đơn giản: chúng tôi chia đầu vào thành các khối dài ( bức xạ+ 1) và sử dụng bảy trong số tám bit trong mỗi khối cho dữ liệu, trong khi bit còn lại (trong cấu trúc này, MSB) xen kẽ giữa việc được đặt cho toàn bộ khối, xóa cho toàn bộ khối tiếp theo, được đặt cho khối sau đó, vân vân Bởi vì các khối dài hơn tham số bức xạ, ít nhất một ký tự từ mỗi khối tồn tại trong đầu ra; vì vậy bằng cách chạy các ký tự có cùng MSB, chúng ta có thể tìm ra khối nào mỗi nhân vật thuộc về. Số lượng khối cũng luôn lớn hơn tham số bức xạ, vì vậy chúng ta luôn có ít nhất một khối không bị hư hại trong encdng; do đó chúng tôi biết rằng tất cả các khối dài nhất hoặc gắn dài nhất đều không bị hư hại, cho phép chúng tôi coi bất kỳ khối ngắn nào là hư hỏng (do đó bị cắt xén). Chúng ta cũng có thể suy ra tham số bức xạ như thế này (nó '
Mã hóa xóa
Đối với phần mã hóa xóa của vấn đề, điều này sử dụng một trường hợp đặc biệt đơn giản của việc xây dựng Reed-Solomon. Đây là một cấu trúc có hệ thống: đầu ra (của thuật toán mã hóa xóa) bằng với đầu vào cộng với một số khối bổ sung, bằng với tham số bức xạ. Chúng ta có thể tính toán các giá trị cần thiết cho các khối này theo cách đơn giản (và golf!), Thông qua việc coi chúng là các khối, sau đó chạy thuật toán giải mã trên chúng để "tái tạo" giá trị của chúng.
Ý tưởng thực tế đằng sau việc xây dựng cũng rất đơn giản: chúng tôi phù hợp với một đa thức, ở mức độ tối thiểu có thể, cho tất cả các khối trong mã hóa (với các lần cắt được nội suy từ các phần tử khác); nếu đa thức là f , khối thứ nhất là f (0), khối thứ hai là f (1), v.v. Rõ ràng là mức độ của đa thức sẽ bằng số khối đầu vào trừ đi 1 (vì chúng ta khớp một đa thức với các khối đó trước, sau đó sử dụng nó để xây dựng các khối "kiểm tra" bổ sung); và bởi vì d +1 điểm xác định duy nhất một đa thức bậc d, cắt xén bất kỳ số khối nào (tối đa tham số bức xạ) sẽ để lại một số khối không bị hư hại bằng với đầu vào ban đầu, đó là thông tin đủ để tái tạo lại đa thức. (Sau đó chúng ta chỉ cần đánh giá đa thức để giải quyết một khối.)
Chuyển đổi cơ sở
Việc xem xét cuối cùng còn lại ở đây là thực hiện với các giá trị thực tế được thực hiện bởi các khối; nếu chúng ta thực hiện phép nội suy đa thức trên các số nguyên, kết quả có thể là số hữu tỷ (chứ không phải số nguyên), lớn hơn nhiều so với các giá trị đầu vào hoặc không mong muốn. Như vậy, thay vì sử dụng các số nguyên, chúng tôi sử dụng trường hữu hạn; trong chương trình này, trường hữu hạn được sử dụng là trường số nguyên modulo p , trong đó p là số nguyên tố lớn nhất nhỏ hơn 128 bức xạ +1(tức là số nguyên tố lớn nhất mà chúng ta có thể khớp với một số giá trị riêng biệt bằng số nguyên tố đó vào phần dữ liệu của một khối). Ưu điểm lớn của các trường hữu hạn là phép chia (trừ 0) được xác định duy nhất và sẽ luôn tạo ra một giá trị trong trường đó; do đó, các giá trị nội suy của đa thức sẽ khớp với một khối giống như cách các giá trị đầu vào thực hiện.
Sau đó, để chuyển đổi đầu vào thành một chuỗi dữ liệu khối, chúng ta cần thực hiện chuyển đổi cơ sở: chuyển đổi đầu vào từ cơ sở 256 thành một số, sau đó chuyển đổi thành cơ sở p (ví dụ: tham số bức xạ là 1, chúng ta có p= 16381). Điều này chủ yếu được duy trì do thiếu các thói quen chuyển đổi cơ sở của Perl (Math :: Prime :: Util có một số, nhưng chúng không hoạt động cho các cơ sở bignum, và một số số nguyên tố chúng tôi làm việc ở đây rất lớn). Vì chúng tôi đã sử dụng Math :: Polynomial cho phép nội suy đa thức, tôi có thể sử dụng lại nó như là một hàm "chuyển đổi từ chuỗi số" (thông qua việc xem các chữ số là hệ số của đa thức và đánh giá nó), và điều này hoạt động cho bignums bình thường. Đi theo con đường khác, mặc dù, tôi đã phải tự viết chức năng. May mắn thay, nó không quá khó (hoặc dài dòng) để viết. Thật không may, chuyển đổi cơ sở này có nghĩa là đầu vào thường được hiển thị không thể đọc được. Cũng có một vấn đề với các số 0 hàng đầu;
Cần lưu ý rằng chúng ta không thể có nhiều hơn khối p trong đầu ra (nếu không các chỉ số của hai khối sẽ trở nên bằng nhau, và có thể cần phải tạo ra các đầu ra khác nhau tạo thành đa thức). Điều này chỉ xảy ra khi đầu vào cực kỳ lớn. Chương trình này giải quyết vấn đề theo một cách rất đơn giản: tăng bức xạ (làm cho các khối lớn hơn và p lớn hơn nhiều, có nghĩa là chúng ta có thể chứa nhiều dữ liệu hơn và dẫn đến kết quả chính xác).
Một điểm đáng làm khác là chúng ta mã hóa chuỗi null thành chính nó, bởi vì chương trình như được viết sẽ bị sập trên nó. Đây rõ ràng cũng là mã hóa tốt nhất có thể, và hoạt động bất kể thông số bức xạ là gì.
Cải tiến tiềm năng
Sự không hiệu quả tiệm cận chính trong chương trình này là để sử dụng modulo-Prime làm các trường hữu hạn trong câu hỏi. Các trường hữu hạn có kích thước 2 n tồn tại (đó chính xác là những gì chúng ta muốn ở đây, bởi vì kích thước tải trọng của các khối tự nhiên là sức mạnh của 128). Thật không may, chúng khá phức tạp hơn so với cách xây dựng modulo đơn giản, có nghĩa là Math :: ModInt sẽ không cắt nó (và tôi không thể tìm thấy bất kỳ thư viện nào trên CPAN để xử lý các trường hữu hạn có kích thước không chính); Tôi phải viết cả một lớp với số học quá tải cho Math :: Polynomial để có thể xử lý nó, và tại thời điểm đó, chi phí byte có thể có khả năng vượt quá tổn thất (rất nhỏ) khi sử dụng, ví dụ, 16381 thay vì 16384.
Một ưu điểm khác của việc sử dụng kích thước power-of-2 là việc chuyển đổi cơ sở sẽ trở nên dễ dàng hơn nhiều. Tuy nhiên, trong cả hai trường hợp, một phương pháp tốt hơn để biểu thị độ dài của đầu vào sẽ hữu ích; phương pháp "trả trước 1 trong các trường hợp mơ hồ" là đơn giản nhưng lãng phí. Chuyển đổi cơ sở sinh học là một cách tiếp cận hợp lý ở đây (ý tưởng là bạn có cơ sở là một chữ số và 0 không phải là một chữ số, sao cho mỗi số tương ứng với một chuỗi).
Mặc dù hiệu suất tiệm cận của mã hóa này rất tốt (ví dụ: đầu vào có độ dài 99 và tham số bức xạ là 3, mã hóa luôn dài 128 byte, thay vì ~ 400 byte mà các phương pháp dựa trên lặp lại sẽ có được), hiệu suất của nó là kém tốt về đầu vào ngắn; độ dài của mã hóa luôn luôn ít nhất là bình phương của (tham số bức xạ + 1). Vì vậy, đối với các đầu vào rất ngắn (chiều dài 1 đến 8) ở bức xạ 9, tuy nhiên độ dài của đầu ra là 100. (Ở độ dài 9, độ dài của đầu ra đôi khi là 100 và đôi khi là 110.) Các cách tiếp cận dựa trên sự lặp lại rõ ràng đã đánh bại việc xóa này phương pháp tiếp cận dựa trên mã hóa trên các đầu vào rất nhỏ; nó có thể có giá trị thay đổi giữa nhiều thuật toán dựa trên kích thước của đầu vào.
Cuối cùng, nó không thực sự xuất hiện trong việc tính điểm, nhưng với các thông số bức xạ rất cao, sử dụng một bit của mỗi byte (kích thước đầu ra) để phân định các khối là lãng phí; Thay vào đó, sẽ rẻ hơn khi sử dụng các dấu phân cách giữa các khối. Tái tạo các khối từ các dấu phân cách khá khó hơn so với cách tiếp cận MSB xen kẽ, nhưng tôi tin rằng điều đó là có thể, ít nhất là nếu dữ liệu đủ dài (với dữ liệu ngắn, khó có thể suy ra tham số bức xạ từ đầu ra) . Đó sẽ là một cái gì đó để xem xét nếu nhắm đến một cách tiếp cận lý tưởng không có triệu chứng bất kể các tham số.
(Và tất nhiên, có thể có một thuật toán hoàn toàn khác tạo ra kết quả tốt hơn thuật toán này!)