Thứ Hai, 17 tháng 2, 2014

Palindrome



Palindrome
 Olympic Quốc tế, năm 2000, Bắc Kinh, Trung Quốc.
Dãy kí tự s được gọi là đối xứng (palindrome) nếu các phần tử cách đều đầu và cuối giống nhau. Cho dãy s tạo bởi n kí tự gồm các chữ cái hoa và thường phân biệt và các chữ số. Hãy cho biết cần xoá đi từ s ít nhất là bao nhiêu kí tự để thu được một dãy đối xứng. Giả thiết rằng sau khi xoá bớt một số kí tự từ s thì các kí tự còn lại sẽ tự động xích lại sát nhau.
Dữ liệu vào ghi trong tệp văn bản PALIN.INP với cấu trúc như sau:
PALIN.INP
PALIN.OUT
9
baeadbadb
4

Dòng đầu tiên là giá trị n, 1 £ n £ 1000.
Dòng thứ hai là n kí tự của dãy viết liền nhau.
Dữ liệu ra ghi trong tệp văn bản PALIN.OUT: số lượng kí tự cần xóa.
Thí dụ, với dãy s gồm 9 kí tự, s = 'baeadbadb' thì cần xoá ít nhất 4 kí tự, chẳng hạn, các kí tự thứ 5, 7, 8 và 9 sẽ thu được dãy đối xứng chiều dài 5 là baeab:
baeadbadb ®  baeab
Dĩ nhiên là có nhiều cách xoá. Thí dụ, có thể xoá các kí tự thứ 2, 3, 4 và 6 từ dãy s để thu được dãy con đối xứng khác là bdadb với cùng chiều dài 5:  
baeadbadb ®  bdadb
Tuy nhiên đáp số là số ít nhất các kí tự cần loại bỏ khỏi s thì là duy nhất và bằng 4.
Bài giải
Bài toán này đã được nhiều bạn đọc công bố lời giải với một mảng hai chiều kích thước n2 hoặc vài ba mảng một chiều kích thước n, trong đó n là chiều dài của dữ liệu vào.
Với một nhận xét nhỏ ta có thể phát hiện ra rằng chỉ cần dùng một mảng một chiều kích thước n và một vài biến đơn là đủ.
Gọi dãy dữ liệu vào là s. Ta tìm chiều dài của dãy con đối xứng v dài nhất trích từ s. Khi đó số kí tự cần xoá từ s sẽ là t = length(s) - length(v). Dãy con ở đây được hiểu là dãy thu được từ s bằng cách xoá đi một số phần tử trong s. Thí dụ với dãy s = baeadbadb thì dãy con đối xứng dài nhất của s sẽ là baeab hoặc bdadb,… Các dãy này đều có chiều dài 5.
Lập hệ thức: Gọi p(i, j) là chiều dài của dãy con dài nhất thu được khi giải bài toán với dữ liệu vào là đoạn s[i..j]. Khi đó p(1, n) là chiều dài của dãy con đối xứng dài nhất trong dãy n kí tự s[1..n] và do đó số kí tự cần loại bỏ khỏi dãy s[1..n] sẽ là
n-p(1,n)
Đó chính là đáp số của bài toán.
Ta liệt kê một số tính chất quan trọng của hàm hai biến p(i, j). Ta có:
-   Nếu i > j, tức là chỉ số đầu trái lớn hơn chỉ số đầu phải, ta quy ước đặt p(i, j) = 0.
-   Nếu i = j thì p(i, i) = 1 vì dãy khảo sát chỉ chứa đúng 1 kí tự nên nó là đối xứng.
-   Nếu i < j và s[i] = s[j] thì p(i, j) = p(i + 1, j – 1) + 2. Vì hai kí tự đầu và cuối dãy s[i,j] giống nhau nên chỉ cần xác định chiều dài của dãy con đối xứng dài nhất trong đoạn giữa là s[i + 1, j – 1] rồi cộng thêm 2 đơn vị ứng với hai kí tự đầu và cuối dãy là được.
-   Nếu i < j và s[i] ¹ s[j], tức là hai kí tự đầu và cuối của dãy con s[i..j] là khác nhau thì ta khảo sát hai dãy con là s[i..(j – 1)] và s[(i + 1)..j] để lấy chiều dài của dãy con đối xứng dài nhất trong hai dãy này làm kết quả:
p(i,j) = max(p(i,j-1),p(i+1,j))
Vấn đề đặt ra là cần tính p(1, n). Mà muốn tính được p(1, n) ta phải tính được các p(i, j) với mọi i, j = 1..n.
Phương án đệ quy
Phương án đệ quy dưới đây như mô tả trong hàm nguyên rec(i, j) tính trực tiếp giá trị p(i, j) theo các tính chất đã liệt kê. Đáp số cho bài toán khi đó sẽ là  n-rec(1,n)
(*------------------------------------
            Phuong an de quy
------------------------------------*)
function rec(i,j: integer): integer;
begin
if i > j then rec  :=  0
else if i = j then rec  :=  1
else {i < j}
if s[i] = s[j]
then rec  :=  rec(i+1,j-1)+2
else {i < j & s[i] ¹ s[j]}
rec  :=  max(rec(i,j-1),rec(i+1,j));
end;



 j-1
 j



b
a
e
a
d
b
a
d
b







u
v
w
x
y
z
{
|
}





b
j
1
1
1
3
3
5
5
5
5





a
k
0
1
1
3
3
3
3
3
3
 i

[i,j-1]
[i,j]

e
l
0
0
1
1
1
1
3
3
3
i+1

[i+1,j-1]
[i+1,j]

a
m
0
0
0
1
1
1
3
3
3





d
n
0
0
0
0
1
1
1
3
3





b
o
0
0
0
0
0
1
1
1
3





a
p
0
0
0
0
0
0
1
1
1





d
q
0
0
0
0
0
0
0
1
1





b
r
0
0
0
0
0
0
0
0
1






Gía trị của hàm p(i,j) đối với dãy baeadbadb
i,j=1..9


Dùng một mảng hai chiều
Gọi đệ quy sẽ phát sinh các lời gọi hàm trùng lặp như đã phân tích trong bài toán 7.1. Ta khắc phục điều này bằng cách sử dụng một mảng hai chiều để tính trước các giá trị của hàm p(i, j), mỗi giá trị được tính tối đa một lần. Nếu dùng một mảng hai chiều, thí dụ mảng p[0..n, 0..n] thì giá trị của p[i, j] sẽ được điền lần lượt theo từng cột, từ cột thứ 1 đến cột thứ n. Tại mỗi cột ta điền từ dưới lên trên. Ta lưu ý:
-       Phần tử tại cột i, dòng j là giá trị p[i, j] chính là chiều dài của dãy con đối xứng dài nhất khi khảo sát dãy con s[i..j].
-       Với các trị i > j, ta quy định p[i, j] = 0. Như vậy nửa tam giác dưới của ma trận p sẽ chứa toàn 0.
-       Nếu i = j thì p[i, j] = 1. Như vậy, mọi trị trên đường chéo chính của ma trận p sẽ là 1.
-       Với các ô còn lại, toạ độ (i, j) sẽ thoả điều kiện i < j, nên p[i, j] sẽ được tính như sau:
if s[i] = s[j] then p[i,j] = p[i+1,j-1]+2
          else p[i,j] :=  max(p[i,j-1],p[i+1,j])
Bạn hãy thử điền một vài giá trị cho bảng trên để rút ra quy luật.
Hãy bắt đầu với cột 1: p[1, 1] = 0;
Sau đó đến cột 2:
p[2, 2] = 1; p[1, 2] = max(p[1, 1], p[2, 2]) = 1, vì s[1] ¹ s[2].
Rồi đến cột 3:
p[3,3]=1; p[2,3] = max(p[2, 2], p[3, 3]) = 1, vì s[2] ¹ s[3];
p[1,3] = max(p[1,2], p[2,3]) = 1, vì s[1] ¹ s[3],…
Dùng hai mảng một chiều
Ta sẽ không theo đuổi phương án dùng mảng hai chiều mà hãy căn cứ vào quy luật điền mảng hai chiều để vận dụng cho hai mảng một chiều là v[0..(n + 1)] và d[0..(n + 1)]. Theo kinh nghiệm, ta nên khai báo kích thước mảng rộng hơn chừng hai phần tử để sử dụng các phần tử này như những lính canh chứa các giá trị khởi đầu phục vụ cho các trường hợp chỉ số i, j nhận các giá trị 0 hoặc n + 1.
Giả sử mảng v chứa các giá trị đã điền của cột j – 1 trong mảng hai chiều p. Ta sẽ điền các giá trị cho cột j của mảng p vào mảng một chiều d. Như vậy, tại bước j, phần tử v[i] sẽ ứng với phần tử p[j – 1, i] còn phần tử d[i] sẽ ứng với p[j, i].  Thủ tục điền trị cho cột d tại bước j dựa theo kết quả lưu trong cột v của bước j – 1 khi đó sẽ như sau:
for i  :=  j-1 downto 1 do
begin
if s[i] = s[j] then d[i] :=  v[i+1]+2
else d[i]  :=  max(v[i],d[i+1]);
end;
Sau mỗi lần lặp với j := 1..n ta chuyển giá trị của d cho v để chuẩn bị cho bước tiếp theo.
(*---------------------------------
      Quy hoach dong voi 2 mang
           1 chieu d, v
----------------------------------*)
procedure QHD2;
var i,j: integer;
begin
fillchar(v,sizeof(v),0);
for j := 1 to n do
begin
d[j] :=  1;
for i  :=  j-1 downto 1 do
begin
  if s[i]= s[j] then d[i] :=  v[i+1]+2
    else d[i] :=  max(v[i],d[i+1]);
end;
v := d;
end;
writeln(nl,n-d[1]); {dap so}
end;
Dùng một mảng một chiều
Có thể chỉ sử dụng một mảng một chiều d cho bài toán này với nhận xét sau đây. Tại bước cập nhật thứ j, với mỗi i = (j – 1)..1 ta có d[i] = p[i, j] và được tính như sau:
s         Nếu s[i] = s[j] thì d[i] tại bước j bằng d[i + 1] tại bước j – 1 cộng với 2.
s         Nếu s[i] ¹ s[j] thì
d[i] tại bước j bằng max(d[i] tại bước j – 1, d[i + 1] tại bước j).
Nếu ta tính từ dưới lên, tức là tính d[i] với i = n..1 thì d[i + 1] cũ sẽ bị ghi đè. Ta dùng hai biến phụ t và tr để bảo lưu giá trị này.
(*---------------------------------
Quy hoach dong voi mang 1 chieu d
----------------------------------*)
procedure QHD1;
var i,j,t,tr: integer;
begin
for j := 1 to n do
begin
tr := 0;
d[j] := 1;
for i := j-1 downto 1 do
begin
  t  :=  d[i];
  if s[i]= s[j] then d[i] :=  tr+2
    else d[i] :=  max(d[i],d[i+1]);
    tr :=  t;
end;
end;
writeln(nl,n-d[1]); {dap so}
end;

Dĩ nhiên phương án dùng một mảng một chiều sẽ được coi trọng vì tiết kiệm được miền nhớ. Tuy nhiên, tinh ý một chút, bạn sẽ phát hiện ra rằng thời gian tính toán theo phương án này không ít hơn phương án dùng hai mảng một chiều. Thật vậy, để tính mỗi phần tử ta phải dùng thêm hai phép gán, trong khi dùng hai mảng một chiều ta chỉ phải thêm một phép gán cho mỗi phần tử. Hơn nữa, dùng hai mảng một chiều thường tránh được nhầm lẫn, do đó nhiều người thường chọn phương án này.
Toàn văn chương trình với ba phương án, đệ quy, dùng hai mảng một chiều và dùng một mảng một chiều khi đó sẽ như sau.
(*  Pascal  *)
uses crt;
const mn = 51;
 bl = #32; nl = #13#10;
 fn = 'palin.inp';
 gn = 'palin.out';
type mi1 = array[0..mn] of integer;
     mi2 = array[0..mn] of mi1;
     mc1 = array[0..mn] of char;
var  n: integer; { Chieu dai xau }
     f,g: text;
     s: mc1; { xau can xu li }
     d,v: mi1;
     c: mi2;
procedure Doc; tự viết
function Max(a,b: integer): integer; tự viết
(*-----------------------------------
                  Phuong an de quy
------------------------------------*)
function rec(i,j: integer): integer; tự viết
(*------------------------------------
   Quy hoach dong voi mang 2 chieu c
-------------------------------------*)
function QHD2C: integer; tự viết
(*---------------------------------
      Quy hoach dong voi 2 mang
           1 chieu d, v
----------------------------------*)
function QHD2DV: integer; tự viết
(*---------------------------------
 Quy hoach dong voi mang 1 chieu d
----------------------------------*)
function QHD1: integer; tự viết
procedure Test;
begin
 Doc;
 writeln(nl,'Phuong an 1: De qui: ',n-rec(1,n));
 writeln(nl,'Phuong an 2: Mang 2 chieu: ',n-QHD2C);
 writeln(nl,'Phuong an 3: Hai Mang 1 chieu D, V: ',n-QHD2DV);
 writeln(nl,'Phuong an 4: Mang 1 chieu D: ',n-QHD1);
end;
BEGIN
 Test;
 readln;
END.

1 nhận xét: