回文压缩


15

挑战

编写一个无损压缩和解压缩ASCII文本的程序。它应该专门用于与回文症(包括不区分大小写和标点符号不敏感的回文)一起使用。以最小的源获得最好的压缩效果。

计分

total_bytes_saved / sqrt(program_size) -最高分获胜

total_bytes_saved压缩后的字符串比原始字符串小多少字节,在下面的测试用例中总计。program_size是压缩和解压缩程序的源代码的字节大小。两者之间共享的代码只需计算一次。

例如,如果有10个测试用例,并且一个100字节的程序在7个测试用例上保存了5个字节,而在其中2个上每个保存了10个字节,但是最后一个测试用例长了2个字节,则该解决方案的得分为5.3。((7 * 5 + 10 * 2 - 2) / sqrt(100) = 5.3

测试用例

  • tacocat
  • toohottohoot
  • todderasesareddot
  • amanaplanacanalpanama
  • wasitacaroracatisaw?
  • Bob
  • IManAmRegalAGermanAmI
  • DogeeseseeGod
  • A Santa at NASA
  • Go hang a salami! I'm a lasagna hog.

规则

  1. 有标准漏洞。
  2. 压缩必须适用于所有可打印的ASCII(包括32-126字节,包括字节)文本字符串,而不仅仅是回文。但是,它实际上不必节省任何输入的空间。
  3. 输出可以是任何字节或字符序列,无论其实现方式或内部表示形式如何(例如,字符串,列表和数组都是公平的游戏)。如果编码为UTF-8,请计算字节数,而不是字符数。除非可能使用的唯一代码点在0到255之间,否则不允许使用宽字符串(例如UTF-16或UTF-32)。
  4. 不允许使用压缩/解压缩内置函数。

为了我们自己的乐趣,请将压缩的字符串与您的源代码一起发布。

更新1:将得分从更改total_bytes_saved / program_sizetotal_bytes_saved / sqrt(program_size),以增加重量以提供更好的压缩效果,而减轻重量以进行积极的高尔夫运动。相应地调整分数。

更新2:固定wasitacaroraratisaw?wasitacaroracatisaw?


2
如果从输入中除去大小写,标点和空格,是否可以保证输入将是严格的回文?编辑:没关系-我认为这wasitacaroraratisaw?是一个反例
Digital Trauma

2
我们应该在输入中支持哪些ASCII字符范围?是[32-126]
Arnauld

1
是的,我认为该1000 *部分不是真正需要的,并且我不认为这会使分数感到更“令人满意”;)
越野选手埃里克(Erik the Outgolfer

1
我们可以使用压缩/解压缩内置功能吗?
林恩

4
由于投入很少,因此做任何聪明事情的空间都不大。至少要多几次是很好的。
user1502040

Answers:


16

JavaScript(ES6),3.143(已保存81个字节,664个字节的程序)

R='replace',S=String.fromCharCode,T=c=>c.charCodeAt(),U='toUpperCase',V='0000000',W=(a,b,c=2)=>a.toString(c).slice(b),X=x=>'0b'+x,Y=a=>[...a].reverse().join``,Z=/[^]/g
C=s=>S(...((Y(q=s[U]()[R](/[^A-Z]/g,m=''))==q?(q=q.slice(0,p=-~q.length/2),p%1&&10):11)+q[R](Z,x=>W(T(x),2))+111+s[R](Z,c=>/[a-z]/.test(c)?W("00",m,m=1):m+(/[A-Z]/.test(c,m='')?"01":W(c<'!'?2:T(c)+384)))+V).match(/(?!0+$).{8}/g).map(X))
D=s=>{s=s[R](Z,c=>W(256+T(c),1))+V;M=r=>(s=s[R](p=s.match(`^${r}|`)[0],''),p);for([,a]=M`1.|0`,t=u=i='';!M`111`;)t+=W(X(M`.{5}`)-~8,0,36);for(t+=W(Y(t),a?a/0:1);p;)u+=M`0(?=00)|00?1`?(c=t[i++])?+p[1]?c[U]():c:'':M`10`?' ':M`11`&&S(X(M`.{7}`));return u+W(t,i)}

现在,我对该程序(和计分系统)非常满意,我将写一些解释。

基本思想是将输入压缩为一串位,然后将每组8位压缩为一个字节。为了说明的目的,我将只操作位字符串。

该位串可以分为几个部分:

input  -> Taco Cat.
output -> 0101000000100011011111110100001100100011101011100000000

0      | 10100 00001 00011 01111 111 | 01 00001 10 01 0001 110101110
header | letter data                 | styling data

标头是一个非常简单的映射:

0  -> odd-length palindrome
10 -> even-length palindrome
11 -> non-palindrome

信函数据也相当简单。首先,从字符串中提取所有非字母,并将所有字母都转换为大写。如果生成的字符串是回文,则将反面的一半剥离。然后应用此映射:

A -> 00001
B -> 00010
C -> 00011
D -> 00100
...
Z -> 11010

本部分以结尾111。之后是样式数据,该数据存储大写/小写数据和非字母数据。这是这样的:

01 -> next letter as uppercase
0...01 (n 0s) -> next (n-1) letters as lowercase
10 -> space
11xxxxxxx -> character with code point 0bxxxxxxx

因此,通过上面显示的示例,我们有

header: 0 -> palindrome
letter data: 10100 00001 00011 01111 111 -> taco
styling data:
  01        -> T
  00001     -> aco
  10        -> <space>
  01        -> C
  0001      -> at
  110101110 -> .

当到达位字符串的末尾时,字母数据中所有剩余的字符都将附加到结果中。这使我们不必进行最后的操作,000...001并允许我们截断字符串中的这些位。

通过测试用例:

tacocat -> 3 bytes (-4)
    24 bits: 010100000010001101111111
toohottohoot -> 5 bytes (-7)
    35 bits: 10101000111101111010000111110100111
todderasesareddot -> 7 bytes (-10)
    49 bits: 0101000111100100001000010110010000011001100101111
amanaplanacanalpanama -> 8 bytes (-13)
    59 bits: 00000101101000010111000001100000110000001011100000100011111
wasitacaroracatisaw? -> 11 bytes (-9)
    84 bits: 010111000011001101001101000000100011000011001001111111000000000000000000001110111111
Bob -> 2 bytes (-1)
    16 bits: 0000100111111101
IManAmRegalAGermanAmI -> 13 bytes (-8)
    98 bits: 00100101101000010111000001011011001000101001110000101100111010100010100101000001010100000010100101
DogeeseseeGod -> 7 bytes (-6)
    54 bits: 000100011110011100101001011001100101111010000000000101
A Santa at NASA -> 8 bytes (-7)
    63 bits: 100000110011000010111010100000011110110010000011000011001010101
Go hang a salami! I'm a lasagna hog. -> 20 bytes (-16)
   154 bits: 1000111011110100000001011100011100001100110000101100000010110101001111010011000000110001100000000111010000110011101001110011000110000000001100000111010111

哇。这种方法给我留下了深刻的印象。我从没想过要像这样进行位打包编码。(我确实想到了将ASCII打包为7位的情况,但并没有为回文节省太多空间)我印象深刻的是,您也设法节省了空间Bob
Beefster

4
这是工程基础知识的一个很好的例子。进行问题描述,考虑解决问题的不同方法,以及在需求之间进行权衡(例如,多少位用于各种样式),等等
罗伯特·弗雷泽

@Beefster谢谢:-) Bob真的只是掉到了位置-1位的标题,10 + 3位的两个字母和2位的大写字母。如果我尽力而为,那就再短不过了
ETHproductions '18

1
@KevinCruijssen的问题是,要添加的内容是字符串,因此必须先将其转换为数字。这种方式比-0+9
ETHproductions'18Feb

1
@ETHproductions当然可以了(没注意到这是一个字符串)!+9会连接到字符串,而-~8会进行+9算术运算(因为-对字符串不做任何事情,因此它将其解释为数字)。在这种情况下-~8非常聪明。:)好的回答顺便说一句,我+1!这样非常聪明地将所有信息存储在位中,甚至在上保存一个字节Bob
凯文·克鲁伊森

2

Python 2:2.765(保存70字节,641字节程序)

我改变了我的方法。现在,它可以很好地用于不完善的回文。没有压缩字符串会长于输入。完美的等长回文将始终压缩到原始大小的50%。

A=lambda x:chr(x).isalpha()
def c(s):
 r=bytearray(s);q=len(r);L=0;R=q-1;v=lambda:R+1<q and r[R+1]<15
 while L<=R:
  while not A(r[L])and L<R:L+=1
  while not A(r[R])and R:
   if v()and r[R]==32:r[R]=16+r.pop(R+1)
   R-=1
  j=r[L];k=r[R]
  if A(j)*A(k):
   if L!=R and j&31==k&31:
    r[L]+=(j!=k)*64;r[R]=1
    if v():r[R]+=r.pop(R+1)
   else:r[L]|=128;r[R]|=128
  L+=1;R-=1
 while r[-1]<16:r.pop()
 return r
def d(s):
 r='';t=[]
 for o in s:
  if 15<o<32:r+=' ';o-=16
  while 0<o<16:r+=chr(t.pop());o-=1
  if o==0:continue
  if 127<o<192:o-=64;t+=[o^32]
  elif o>192:o-=128
  elif A(o):t+=[o]
  r+=chr(o)
 while t:r+=chr(t.pop())
 return r

结果

'tacocat' <==> 'tac\xef'
4/7 (3 bytes saved)
'toohottohoot' <==> 'toohot'
6/12 (6 bytes saved)
'todderasesareddot' <==> 'todderas\xe5'
9/17 (8 bytes saved)
'amanaplanacanalpanama' <==> 'amanaplana\xe3'
11/21 (10 bytes saved)
'wasitacaroracatisaw?' <==> 'wasita\xe3ar\xef\x09?'
12/20 (8 bytes saved)
'Bob' <==> '\x82\xef'
2/3 (1 bytes saved)
'IManAmRegalAGermanAmI' <==> 'I\x8d\xa1n\x81m\x92e\xa7\xa1\xec'
11/21 (10 bytes saved)
'Dogeeseseegod' <==> '\x84ogees\xe5'
7/13 (6 bytes saved)
'A Santa at NASA' <==> 'A S\xa1\xaeta\x12\x14'
9/15 (6 bytes saved)
"Go hang a salami! I'm a lasagna hog." <==> "\x87o hang a salam\xa9!\x11'\x01\x11\x17\x13."
24/36 (12 bytes saved)

另外,它还节省了我以前不正确的回文所需要的6个字节。

'wasita\xe3ar\xef\x02\xf2\x06?' <==> 'wasitacaroraratisaw?'
6 bytes saved

说明

解压缩使用堆栈。从32-127的代码点按字面意义对待。如果字符是字母,则还将一个值压入堆栈。值128-192用于大小写翻转的字母,因此,大小写翻转的字母(o^32由于ASCII的布局方式)被压入堆栈,普通字母被添加到字符串中。值192-255用于添加字母而不会压入堆栈,因此当字母不匹配时以及奇数回文中的中间字母都使用此值。代码点1-15指示应将堆栈弹出该次数。代码点17-31相似,但是它们在从堆栈中弹出之前先打印一个空格。输入末尾还有一条隐式的“弹出直到空”指令。

压缩器从两端开始工作,并以匹配的字母折叠为值1-31。它会跳过非字母。当字母匹配但大小写不匹配时,它将在左字母上添加64,并在右字母上递增。这样可以节省的空间IManAmRegalAGermanAmI。在中间或字母不匹配时,在两侧都为128。我无法在此处添加内容,因为我需要避免特殊情况where left == right。在右侧折叠相邻的弹出标记时,我必须检查相邻的弹出标记是否不会溢出到代码点16中,因为我需要空格。(这实际上不是任何测试用例字符串的问题)

编辑1:没有更多的版本。


1

Python3,1.833(已保存25个字节,186个字节的程序)

只是简单的0阶等概率熵编码。没有回文特定的优化。

def C(s):
    u=0
    for c in s:u=u*96+ord(c)-31
    return u.to_bytes((u.bit_length()+7)//8,'big')
def D(a):
    u,s=int.from_bytes(a,'big'),''
    while u:s,u=s+chr((u%96)+31),u//96
    return s[::-1]

0

Java 8,成绩:1.355(节省20字节/ 218(107 + 111)字节)

压缩功能(包含三个不可打印的ASCII字符):

s->{int l=s.length();return s.contains(new StringBuffer(s).reverse())?s.substring(l/2)+(l%2<1?"":""):s;}

解压缩功能(包含两个不可打印的ASCII字符):

s->{return s.contains("")?new StringBuffer((s=s.replaceAll("","")).substring(s.length()&1^1)).reverse()+s:s;}

说明:

在线尝试。

仅压缩完美回文。

s->{                      // Method with String as both parameter and return-type
  int l=s.length();       //  Get the length of the input
  return s.contains(new StringBuffer(s).reverse())?
                          //  If the input is a palindrome:
    s.substring(l/2)      //   Only return the second halve of the String
    +(l%2<1?"":"")        //   + either one (if even) or two (if odd) unprintables 
   :                      //  Else:
    s;}                   //   Simply return the input again

s->{                      // Method with String as both parameter and return-type
  return s.contains("")?  //  If the input contains an unprintable:
    new StringBuffer((s=s.replaceAll("",""))
                          //   Remove the unprintables
                     .substring(s.length()&1^1))
                          //   And take either the full string (if even),
                          //   or minus the first character (if odd)
    .reverse()            //    And reverse that part
    +s                    //   And append the rest of the input (minus the unprintables)
   :                      //  Else:
    s;}                   //   Simply return the input again
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.