Giải thuật và lập trình - 21


STR.INP

STR.OUT

PBBCEFATZQABCDABEFA

7

PBBCEFATZ -> Delete(9) -> PBBCEFAT PBBCEFAT -> Delete(8) -> PBBCEFA PBBCEFA -> Insert(4, B) -> PBBCBEFA PBBCBEFA -> Insert(4, A) -> PBBCABEFA PBBCABEFA -> Insert(4, D) -> PBBCDABEFA

PBBCDABEFA -> Replace(2, A) -> PABCDABEFA

PABCDABEFA -> Replace(1, Q) -> QABCDABEFA

Có thể bạn quan tâm!

Xem toàn bộ 316 trang tài liệu này.

Giải thuật và lập trình - 21

Cách giải:

Đối với xâu ký tự thì việc xoá, chèn sẽ làm cho các phần tử phía sau vị trí biến đổi bị đánh chỉ số lại, gây khó khăn cho việc quản lý vị trí. Để khắc phục điều này, ta sẽ tìm một thứ tự biến đổi thoả mãn: Phép biến đổi tại vị trí i bắt buộc phải thực hiện sau các phép biến đổi tại vị trí i

+ 1, i + 2, …

Ví dụ: X = 'ABCD';

Insert(0, E) sau đó Delete(4) cho ra X = 'EABD'. Cách này không tuân thủ nguyên tắc Delete(3) sau đó Insert(0, E) cho ra X = 'EABD'. Cách này tuân thủ nguyên tắc đề ra. Nói tóm lại ta sẽ tìm một dãy biến đổi có vị trí thực hiện giảm dần.


3.3.1. Công thức truy hồi

Giả sử m là độ dài xâu X và n là độ dài xâu Y. Gọi F[i, j] là số phép biến đổi tối thiểu để biến xâu gồm i ký tự đầu của xâu X: X1X2 … Xi thành xâu gồm j ký tự đầu của xâu Y: Y1Y2…Yj. Quan sát hai dãy X và Y

X1

X2

Xm-1

Xm


Y1

Y2

Yn-1

Yn

Ta nhận thấy:

Nếu Xm = Yn thì ta chỉ cần biến đoạn X1X2…Xm-1 thành Y1Y2…Yn-1


X1

X2

Xm-1

Xm=Yn


Y1

Y2

Yn-1

Yn=Xm

Tức là trong trường hợp này: F[m, n] = F[m - 1, n - 1]


Nếu Xm Yn thì tại vị trí Xm ta có thể sử dụng một trong 3 phép biến đổi:

a) Hoặc chèn vào sau vị trí m của X, một ký tự đúng bằng Yn:


X1

X2

Xm-1

Xm

Yn


Y1

Y2

Yn-1

Yn

Thì khi đó F[m, n] sẽ bằng 1 phép chèn vừa rồi cộng với số phép biến đổi biến dãy X1…Xm thành dãy Y1…Yn-1: F[m, n] = 1 + F[m, n - 1]

b) Hoặc thay vị trí m của X bằng một ký tự đúng bằng Yn:


X1

X2

Xm-1

Xm:=Yn

Y1

Y2

Yn-1

Yn

Thì khi đó F[m, n] sẽ bằng 1 phép thay vừa rồi cộng với số phép biến đổi biến dãy X1…Xm-1 thành dãy Y1…Yn-1: F[m, n] = 1 + F[m-1, n - 1]

c) Hoặc xoá vị trí thứ m của X:


X1

X2

Xm-1

Xm

Y1

Y2

Yn-1

Yn

Thì khi đó F[m, n] sẽ bằng 1 phép xoá vừa rồi cộng với số phép biến đổi biến dãy X1…Xm-1 thành dãy Y1…Yn: F[m, n] = 1 + F[m-1, n]

Vì F[m, n] phải là nhỏ nhất có thể, nên trong trường hợp Xm Yn thì F[m, n] = min(F[m, n - 1], F[m - 1, n - 1], F[m - 1, n]) + 1.

Ta xây dựng xong công thức truy hồi.


3.3.2. Cơ sở quy hoạch động

F[0, j] là số phép biến đổi biến xâu rỗng thành xâu gồm j ký tự đầu của F. Nó cần tối thiểu j phép chèn: F[0, j] = j

F[i, 0] là số phép biến đổi biến xâu gồm i ký tự đầu của S thành xâu rỗng, nó cần tối thiểu i phép xoá: F[i, 0] = i

Vậy đầu tiên bảng phương án F (cỡ[0..m, 0..n]) được khởi tạo hàng 0 và cột 0 là cơ sở quy hoạch động. Từ đó dùng công thức truy hồi tính ra tất cả các phần tử bảng B.

Sau khi tính xong thì F[m, n] cho ta biết số phép biến đổi tối thiểu.

Truy vết:

Nếu Xm = Yn thì chỉ việc xét tiếp F[m - 1, n - 1]. Nếu không, xét 3 trường hợp:

Nếu F[m, n] = F[m, n - 1] + 1 thì phép biến đổi đầu tiên được sử dụng là: Insert(m, Yn)

Nếu F[m, n] = F[m - 1, n - 1] + 1 thì phép biến đổi đầu tiên được sử dụng là: Replace(m, Yn) Nếu F[m, n] = F[m - 1, n] + 1 thì phép biến đổi đầu tiên được sử dụng là: Delete(m)

Đưa về bài toán với m, n nhỏ hơn truy vết tiếp cho tới khi về F[0, 0] Ví dụ: X =' ABCD'; Y = 'EABD' bảng phương án là:


F

0

1

2

3

4

0

0

1

2

3

4

1

1

1

1

2

3

2

2

2

2

1

2

3

3

3

3

2

2

4

4

4

4

3

2


Hình 50: Truy vết


Lưu ý: khi truy vết, để tránh truy nhập ra ngoài bảng, nên tạo viền cho bảng.


P_3_03_4.PAS * Biến đổi xâu

program StrOpt; const

InputFile = 'STR.INP'; OutputFile = 'STR.OUT'; max = 100;

var

X, Y: String[2 * max];

F: array[-1..max, -1..max] of Integer; m, n: Integer;


procedure Enter; var

fi: Text; begin

Assign(fi, InputFile); Reset(fi); ReadLn(fi, X); ReadLn(fi, Y); Close(fi);

m := Length(X); n := Length(Y); end;


function Min3(x, y, z: Integer): Integer; {Cho giá trị nhỏ nhất trong 3 giá trị x, y, z}

var

t: Integer; begin

if x < y then t := x else t := y; if z < t then t := z;

Min3 := t; end;


procedure Optimize; var

i, j: Integer; begin

{Khởi tạo viền cho bảng phương án}

for i := 0 to m do F[i, -1] := max + 1; for j := 0 to n do F[-1, j] := max + 1;

{Lưu cơ sở quy hoạch động}

for j := 0 to n do F[0, j] := j; for i := 1 to m do F[i, 0] := i;

{Dùng công thức truy hồi tính toàn bảng phương án}

for i := 1 to m do for j := 1 to n do

if X[i] = Y[j] then F[i, j] := F[i - 1, j - 1]

else F[i, j] := Min3(F[i, j - 1], F[i - 1, j - 1], F[i - 1, j]) + 1;

end;

procedure Trace; {Truy vết}

var

fo: Text; begin

Assign(fo, OutputFile); Rewrite(fo);

WriteLn(fo, F[m, n]); {F[m, n] chính là số ít nhất các phép biến đổi cần thực hiện}

while (m <> 0) or (n <> 0) do {Vòng lặp kết thúc khi m = n = 0}

if X[m] = Y[n] then {Hai ký tự cuối của 2 xâu giống nhau}

begin

Dec(m); Dec(n); {Chỉ việc truy chéo lên trên bảng phương án}

end

else {Tại đây cần một phép biến đổi}

begin

Write(fo, X, ' -> '); {In ra xâu X trước khi biến đổi}

if F[m, n] = F[m, n - 1] + 1 then {Nếu đây là phép chèn}

begin

Write(fo, 'Insert(', m, ', ', Y[n], ')'); Insert(Y[n], X, m + 1);

Dec(n); {Truy sang phải}

end else

if F[m, n] = F[m - 1, n - 1] + 1 then {Nếu đây là phép thay}

begin

Write(fo, 'Replace(', m, ', ', Y[n], ')'); X[m] := Y[n];

Dec(m); Dec(n); {Truy chéo lên trên}

end

else {Nếu đây là phép xoá}

begin

Write(fo, 'Delete(', m, ')'); Delete(X, m, 1);

Dec(m); {Truy lên trên}

end;

WriteLn(fo, ' -> ', X); {In ra xâu X sau phép biến đổi}

end;

Close(fo);

end;


begin

Enter;

Optimize;

Trace;

end.

Bài này giải với các xâu 100 ký tự, nếu lưu bảng phương án dưới dạng mảng cấp phát động thì có thể làm với các xâu 255 ký tự. (Tốt hơn nên lưu mỗi dòng của bảng phương án là một mảng cấp phát động 1 chiều). Hãy tự giải thích tại sao khi giới hạn độ dài dữ liệu là 100, lại phải khai báo X và Y là String[200] chứ không phải là String[100] ?.

3.4. DÃY CON CÓ TỔNG CHIA HẾT CHO K

Cho một dãy gồm n (1 n 1000) số nguyên dương A1, A2, …, An và số nguyên dương k (k

50). Hãy tìm dãy con gồm nhiều phần tử nhất của dãy đã cho sao cho tổng các phần tử của dãy con này chia hết cho k.

Input: file văn bản SUBSEQ.INP

Dòng 1: Chứa số n

Dòng 2: Chứa n số A1, A2, …, An cách nhau ít nhất một dấu cách

Output: file văn bản SUBSEQ.OUT

Dòng 1: Ghi độ dài dãy con tìm được

Các dòng tiếp: Ghi các phần tử được chọn vào dãy con

Dòng cuối: Ghi tổng các phần tử của dãy con đó.


SUBSEQ.INP

SUBSEQ.OUT

10 5

8

1 6 11 5 10 15 20 2 4 9

a[10] = 9


a[9] = 4


a[7] = 20


a[6] = 15


a[5] = 10


a[4] = 5


a[3] = 11


a[2] = 6


Sum = 80

3.4.1. Cách giải 1

Đề bài yêu cầu chọn ra một số tối đa các phần tử trong dãy A để được một dãy có tổng chia hết cho k, ta có thể giải bài toán bằng phương pháp duyệt tổ hợp bằng quay lui có đánh giá nhánh cận nhằm giảm bớt chi phí trong kỹ thuật vét cạn. Dưới đây ta trình bày phương pháp quy hoạch động:

Nhận xét 1: Không ảnh hưởng đến kết quả cuối cùng, ta có thể đặt:

Ai := Ai mod k với i: 1 i n

Nhận xét 2: Gọi S là tổng các phần tử trong mảng A, ta có thể thay đổi cách tiếp cận bài toán: thay vì tìm xem phải chọn ra một số tối đa những phần tử để có tổng chia hết cho k, ta sẽ chọn ra một số tối thiểu các phần tử có tổng đồng dư với S theo modul k. Khi đó chỉ cần loại bỏ những phần tử này thì những phần tử còn lại sẽ là kết quả.

Nhận xét 3: Số phần tử tối thiểu cần loại bỏ bao giờ cũng nhỏ hơn k

Thật vậy, giả sử số phần tử ít nhất cần loại bỏ là m và các phần tử cần loại bỏ là Ai1, Ai2, …, Aim. Các phần tử này có tổng đồng dư với S theo mô-đun k. Xét các dãy sau

Dãy 0 := () = Dãy rỗng (Tổng 0 (mod k))

Dãy 1 := (Ai1) Dãy 2 := (Ai1, Ai2)

Dãy 3 := (Ai1, Ai2, Ai3)

… …

Dãy m := (Ai1, Ai2, …, Aim)

Như vậy có m + 1 dãy, nếu m k thì theo nguyên lý Dirichlet sẽ tồn tại hai dãy có tổng đồng dư theo mô-đun k. Giả sử đó là hai dãy:

Ai1 + Ai2 + … + Aip Ai1 + Ai2 + … + Aip + Aip+1 + … + Aiq (mod k)

Suy ra Aip+1 + … + Aiq chia hết cho k. Vậy ta có thể xoá hết các phần tử này trong dãy đã chọn mà vẫn được một dãy có tổng đồng dư với S theo modul k, mâu thuẫn với giả thiết là dãy đã chọn có số phần tử tối thiểu.

Công thức truy hồi:

Nếu ta gọi F[i, t] là số phần tử tối thiểu phải chọn trong dãy A1, A2, …, Ai để có tổng chia k dư t. Nếu không có phương án chọn ta coi F[i, t] = + . Khi đó F[i, t] được tính qua công thức truy hồi sau:

Nếu trong dãy trên không phải chọn Ai thì F[i, t] = F[i - 1, t];


Nếu trong dãy trên phải chọn Ai thì F[i, t] = 1 + F[i - 1, t Ai ] ( t Ai


trên các lớp đồng dư mod k. Ví dụ khi k = 7 thì 1 3 5 ) Từ trên suy ra F[i, t] = min (F[i - 1, t], 1 + F[i - 1, t - Ai]).

ở đây hiểu là phép trừ

Còn tất nhiên, cơ sở quy hoạch động: F(0, 0) = 0; F(0, i) = + (với i: 1 i < k). Bảng phương án F có kích thước [0..n, 0.. k - 1] tối đa là 1001x50 phần tử kiểu Byte.

P_3_03_5.PAS * Dãy con có tổng chia hết cho k

program SubSequence; const

InputFile = 'SUBSEQ.INP'; OutputFile = 'SUBSEQ.OUT'; maxN = 1000;

maxK = 50; var

a: array[1..maxN] of Integer;

f: array[0..maxN, 0..maxK - 1] of Byte; n, k: Integer;


procedure Enter; var

fi: Text; i: Integer;

begin

Assign(fi, InputFile); Reset(fi); ReadLn(fi, n, k);

for i := 1 to n do Read(fi, a[i]); Close(fi);

end;


function Sub(x, y: Integer): Integer; {Tính x - y (theo mod k)}

var

tmp: Integer; begin

tmp := (x - y) mod k;

if tmp >= 0 then Sub := tmp else Sub := tmp + k;

end;


procedure Optimize; var

i, t: Integer; begin

FillChar(f, SizeOf(f), $FF); {Khởi tạo các phần tử f[0, .] đều bằng 255 (+)}

f[0, 0] := 0; {Ngoại trừ f[0, 0] := 0}

for i := 1 to n do

for t := 0 to k - 1 do {Tính f[i, t] := min (f[i - 1, t], f[i - 1, Sub(t, ai)] + 1}

if f[i - 1, t] < f[i - 1, Sub(t, a[i])] + 1 then f[i, t] := f[i - 1, t]

else

f[i, t] := f[i - 1, Sub(t, a[i])] + 1;

end;


procedure Result; var

fo: Text;

i, t: Integer;

SumAll, Sum: LongInt;

begin

SumAll := 0;

for i := 1 to n do SumAll := SumAll + a[i]; Assign(fo, OutputFile); Rewrite(fo);

WriteLn(fo, n - f[n, SumAll mod k]); {n - số phần tử bỏ đi = số phần tử giữ lại}

i := n; t := SumAll mod k;

Sum := 0;

for i := n downto 1 do

if f[i, t] = f[i - 1, t] then {Nếu phương án tối ưu không bỏ ai, tức là có chọn ai}

begin

WriteLn(fo, 'a[', i, '] = ', a[i]); Sum := Sum + a[i];

end else

t := Sub(t, a[i]);

WriteLn(fo, 'Sum = ', Sum);

Close(fo);

end;


begin

Enter;

Optimize;

Result;

end.


3.4.2. Cách giải 2

Phân các phần tử trong dãy a theo các lớp đồng dư modul k. Lớp i gồm các phần tử chia k dư

i. Gọi Count[i] là số lượng các phần tử thuộc lớp i.

Với 0 i, t < k; Gọi f[i, t] là số phần tử nhiều nhất có thể chọn được trong các lớp 0, 1, 2, …, i để được tổng chia k dư t. Trong trường hợp có cách chọn, gọi Trace[i, t] là số phần tử được chọn trong lớp i theo phương án này, trong trường hợp không có cách chọn, Trace[i, t] được coi là -1.

Ta dễ thấy rằng f[0, 0] = Count[0], Trace[0, 0] = Count[0], còn Trace[0, i] với i0 bằng -1. Với i 1; 0 t < k, nếu có phương án chọn ra nhiều phần tử nhất trong các lớp từ 0 tới i để được tổng chia k dư t thì phương án này có thể chọn j phần tử của lớp i (0 j Count[i]), nếu bỏ j phần tử này đi, sẽ phải thu được phương án chọn ra nhiều phần tử nhất trong các lớp từ 0

tới i - 1 để được tổng chia k dư t i * j. Từ đó suy ra công thức truy hồi:


f[i, t]

max

0 jCount[i]


(f[i 1, t j* i] j)

Trace[i1,t j*i)1

Trace[i, t]

arg max

0 jCount[i]


(f[i 1, t j* i] j)

Trace[i1,t j*i)1


P_3_03_6.PAS * Dãy con có tổng chia hết cho k

program SubSequence; const

InputFile = 'SUBSEQ.INP'; OutputFile = 'SUBSEQ.OUT'; maxN = 1000;

maxK = 50; var

a: array[1..maxN] of Integer;

Count: array[0..maxK - 1] of Integer;

f, Trace: array[0..maxK - 1, 0..maxK - 1] of Integer; n, k: Integer;


procedure Enter; var

fi: Text; i: Integer;

begin

Assign(fi, InputFile); Reset(fi); ReadLn(fi, n, k);

FillChar(Count, SizeOf(Count), 0); for i := 1 to n do

begin

Read(fi, a[i]);

Inc(Count[a[i] mod k]); {Nhập dữ liệu đồng thời với việc tính các Count[.]}

end;

Close(fi);

end;


function Sub(x, y: Integer): Integer; var

tmp: Integer; begin

tmp := (x - y) mod k;

if tmp >= 0 then Sub := tmp else Sub := tmp + k;

end;


procedure Optimize; var

i, j, t: Integer; begin

FillChar(f, SizeOf(f), 0); f[0, 0] := Count[0];

FillChar(Trace, SizeOf(Trace), $FF); {Khởi tạo các mảng Trace=-1}

Trace[0, 0] := Count[0]; {Ngoại trừ Trace[0, 0] = Count[0]}

for i := 1 to k - 1 do for t := 0 to k - 1 do

for j := 0 to Count[i] do

if (Trace[i - 1, Sub(t, j * i)] <> -1) and

(f[i, t] < f[i - 1, Sub(t, j * i)] + j) then begin

f[i, t] := f[i - 1, Sub(t, j * i)] + j; Trace[i, t] := j;

end;

end;

..... Xem trang tiếp theo?
⇦ Trang trước - Trang tiếp theo ⇨

Ngày đăng: 06/02/2024