Bài toán tháp hà nội tính số đĩa còn lại

Cần chuyển n đĩa trên từ cột A sang cột B (lấy C làm cột trung gian) theo quy tắc:

  • Mỗi lần chỉ chuyển một đĩa.
  • Đĩa nhỏ phải nằm trên đĩa lớn tại bất kỳ thời điểm nào trong quá trình chuyển.

Để giải bài toán trên, người ta thường dùng giải thuật đệ quy. Giải thuật này cho kết quả là 2^n-1 bước chuyển, là số bước chuyển ít nhất cần thực hiện.

Sau khi học xong giải thuật đệ quy trên, vị thần trong cây đèn của Aladin lập tức bay ra Hà Nội để chuyển đĩa thử. Tuy nhiên mới chỉ thực hiện được k bước chuyển (k ≤ 2^n-1) thì vị thần đói bụng quá nên tạm nghỉ để đi ăn. Bạn hãy tính xem tại lúc này, số đĩa hiện có trên mỗi cột là bao nhiêu.

Dữ liệu nhập:

  • Hai số nguyên n và k cách nhau một khoảng trắng ((1 ≤ n ≤ 10, 1 ≤ k ≤ 2^n -1).

Dữ liệu xuất:

  • Ba số nguyên a, b, c thể hiện số đĩa trên các cột A, B, C sau k bước chuyển. Ba số cách nhau một khoảng trắng.
    Em cảm ơn ạ

Giả thiết: Cột ban đầu có tháp n tầng. 2 cột còn lại “trống” (hoặc có đĩa rất lớn)
Yêu cầu: Chuyển tháp ở cột ban đầu qua cột đích
Đệ quy gồm 3 bước:

  1. Chuyển tháp n-1 tầng từ cột ban đầu qua cột trung gian
  2. Bỏ đĩa cuối cùng vào vị trí đích
  3. Chuyển tháp n-1 tầng từ cột trung gian qua cột đích.
    Code C++ sau sẽ in ra hướng dẫn từng bước để xây tháp:
using namespace std;
typedef enum pillar
{
	Pillar_A = 0,
	Pillar_B = 1,
	Pillar_C = 2
};
void BuildHanoiTower(pillar origin, pillar middle, pillar destiny, unsigned short towerHeight)
{
	static int i = 0; //i dùng để đếm số bước
	if (towerHeight == 1)
	{
		i++;
		printf("Step %d: move 1 plate from pillar %d to %d\n", i, origin, destiny);
		return;
	}
	BuildHanoiTower(origin, destiny, middle, towerHeight - 1);
	i++;
	printf("Step %d: move 1 plate from pillar %d to %d\n", i, origin, destiny);
	BuildHanoiTower(middle, origin, destiny, towerHeight - 1);
	return;
};

Code C++ in ra số đĩa trên từng cột ở bước thứ k

using namespace std;
typedef enum pillar
{
	Pillar_A = 0,
	Pillar_B = 1,
	Pillar_C = 2
};
void BuildHanoiTower(pillar origin, pillar middle, pillar destiny, unsigned short towerHeight, unsigned short k)
{
	static int i = 0; //Biến đếm số bước
	static int plate_num[] = {towerHeight, 0 ,0 }; //Mảng 3 phần tử: số đĩa trên 3 cột
	
	if (towerHeight == 1)
	{
		i++;
		plate_num[origin]--;
		plate_num[destiny]++;
		if (i == k)
		{
			printf("Cot A: %d\nCot B: %d\nCot C: %d\n", plate_num[0], plate_num[1], plate_num[2]);
		}
		return;
	}
	BuildHanoiTower(origin, destiny, middle, towerHeight - 1, k);
	i++;
	plate_num[origin]--;
	plate_num[destiny]++;
	if (i == k)
	{
		printf("Cot A: %d\nCot B: %d\nCot C: %d\n", plate_num[0], plate_num[1], plate_num[2]);
	}
	BuildHanoiTower(middle, origin, destiny, towerHeight - 1, k);
	return;
};
1 Like

Công thức hồi quy T(n):

  • T(n) = T(n-1) + 1 + T(n-1)

Giả sử có 3 cột được đánh số lần lượt là 0, 1, 2. Ban đầu cột 0 có n đĩa, cột 2 có 0 đĩa, cột 3 có 0 đĩa. Công việc là chuyển n đĩa từ cột 0 sang cột 2:

  • Bước 0:
    • Chưa thực hiện di chuyển
    • Cột 0 có n đĩa, cột 1 có 0 đĩa, cột 2 có 0 đĩa
  • Bước T(n-1):
    • Chuyển n-1 đĩa từ cột 0 sang cột 1, cột 2 là cột trung gian
    • Cột 0 có 1 đĩa, cột 1 có n-1 đĩa, cột 2 có 0 đĩa
  • Bước T(n-1) + 1:
    • Chuyển đĩa thứ n từ cột 0 sang cột 2
    • Cột 0 có 0 đĩa, cột 1 có n-1 đĩa, cột 2 có 1 đĩa
  • Bước T(n-1) + 1 + T(n-1):
    • Chuyển n-1 từ cột 1 sang cột 2, cột 0 là cột trung gian
    • Cột 0 có 0 đĩa, cột 1 có 0 đĩa, cột 2 có n đĩa

Tổng quát hơn, 3 cột được đánh số id0, id1, id2, nhận giá trị trong tập { 0; 1; 2 }. Ban đầu cột id0 có d0 đĩa, cột id1 có d1 đĩa, cột id2 có d2 đĩa. Công việc là chuyển n đĩa từ cột id0 sang cột id2: (d0, d1, d2, n >= 0, n <= d0)

  • Bước 0:
    • Chưa thực hiện di chuyển
    • Cột id0d0 đĩa, cột id1d1 đĩa, cột id2d2 đĩa
  • Bước T(n-1):
    • Chuyển n-1 đĩa từ cột id0 sang cột id1, cột id2 là cột trung gian
    • Cột id0d0 - (n-1) đĩa, cột id1d1 + (n-1) đĩa, cột id2d2 đĩa
  • Bước T(n-1) + 1:
    • Chuyển đĩa thứ n từ cột id0 sang cột id2
    • Cột id0d0 - n đĩa, cột id1d1 + (n-1) đĩa, cột 2 có d2 + 1 đĩa
  • Bước T(n-1) + 1 + T(n-1):
    • Chuyển n-1 từ cột id1 sang cột id2, cột id0 là cột trung gian
    • Cột id0d0 - n đĩa, cột id1d1 đĩa, cột id2d2 + n đĩa

Chứng minh toán học

Giả sử T(n) là số lần di chuyển n đĩa ít nhất mà vẫn thoả mãn điều kiện đề bài.

Thử với các giá trị n nhỏ:

  • n = 0, T(0) = 0, T(0) + 1 = 1 = 20
  • n = 1, T(1) = 1, T(1) + 1 = 2 = 21
  • n = 2, T(2) = 3, T(2) + 1 = 4 = 22

Nếu đặt S(n) = T(n) + 1, thì S(n) là cấp số nhân với số hạng đầu là 1, công bội là 2.

  • Công thức tổng quát S(n) = 1.2n = 2n
  • Công thức tổng quát của T(n) = S(n) - 1 = 2n - 1

Mặt khác, triển khai T(n):

  • T(n) = 2n - 1
  • T(n) = 2.2n-1 - 2 + 1
  • T(n) = 2(2n-1 - 1) + 1
  • T(n) = 2T(n-1) + 1

Do đó, công thức hồi quy của T(n) như sau:

  • T(0) = 0
  • T(n) = 2T(n-1) + 1, n > 0

Giải thuật

Với input n và k.

Nếu k < T(n-1):

  • Bài toán từ input (n, k) thành bài toán nhỏ hơn có input (n-1, k) từ cột id0 sang cột id1 với cột id2 là cột trung gian.

Nếu k = T(n-1), chỉ có 1 lần di chuyển:

  • Di chuyển k = T(n-1) lần, cấu hình (d0, d1, d2) thành (d0 - (n-1), d1 + (n-1), d2).

Nếu T(n-1) < k < T(n), có 2 lần di chuyển:

  • Di chuyển T(n-1) + 1 lần từ cấu hình (d0, d1, d2) sang cấu hình (d0 - n, d1 + (n-1), d2 + 1).
  • Di chuyển k - (T(n-1)+1) lần từ cột id1 sang cột id2 với cột id0 là cột trung gian.
  • Bài toán từ input (n, k) thành bài toán nhỏ hơn có input (n-1, k - T(n-1)) từ cột id1 sang cột id2 với cột id0 là cột trung gian.

Nếu k = T(n) = T(n-1) + 1 + T(n-1), có 1 lần di chuyển:

  • Di chuyển T(n-1) + 1 + T(n-1) lần từ cấu hình (d0, d1, d2) sang cấu hình (d0 - n, d1, d2 + n).

Mã nguồn (JavaScript)

Input:

  • n : số nguyên biểu hiện số đĩa cần di chuyển.
  • k : số nguyên biểu hiện số bước mà vị thần đã di chuyển.

Các biến sử dụng:

  • d : là mảng số nguyên gồm 3 phần tử
    • d[0] : số đĩa có ở cột 1, giống d0
    • d[1] : số đĩa có ở cột 2, giống d1
    • d[2] : số đĩa có ở cột 3, giống d2
  • id: mảng số nguyên 3 phần tử chỉ với index của d, nhận giá trị trong { 0; 1; 2 }, đóng vai trò như id0, id1, id2
  • Tc : số bước di chuyển ít nhất, tương ứng T(n)
  • Tp : số bước di chuyển ít nhất, tương ứng T(n-1)
    • Tp được tính theo Tc dựa trên công thức truy hồi, trong code tính theo công thức T(n)/2 thay vì (T(n) - 1) / 2.

Chú thích:

  • Từ bước 0 tới bước T(n-1)-1 tương ứng với điều kiện k < Tp
  • Bước T(n-1) tương ứng điều kiện k == Tp
  • Bước T(n-1)+1 tới T(n)-1 tương ứng với phần else
  • Bước T(n) tương ứng với điều kiện k == Tc
  • Code chưa xử lý edge case.

function towerOfHanoi(n, k) {
  let d = [n, 0, 0];
  let id = [0, 1, 2];

  let Tc = Math.pow(2, n) - 1;
  let Tp = Math.floor(Tc / 2);

  while (k > 0) {
    if (k < Tp) {
      Tc = Tp;
      Tp = Math.floor(Tc / 2);

      // [ id[0], id[1], id[2] ] = [ id[0], id[2], id[1] ];
      [ id[1], id[2] ] = [ id[2], id[1] ];
    } else if (k == Tp) {
      k = 0;
      d[id[0]] -= n-1;
      d[id[1]] += n-1;
    } else if (k == Tc) {
      k = 0;
      d[id[0]] -= n;
      d[id[2]] += n;
    } else { // Tp+1 <= k < Tc
      k-= Tp + 1;
      Tc = Tp;
      Tp = Math.floor(Tc / 2);

      d[id[0]] -= n;
      d[id[1]] += n-1;
      d[id[2]] += 1;
      
      // [ id[0], id[1], id[2] ] = [ id[1], id[0], id[2] ];
      [ id[0], id[1] ] = [ id[1], id[0] ];
    }

    n--;
  }

  return d;
}

for (let i = 0; i <= 31; ++i) {
  const [ d1, d2, d3 ] = towerOfHanoi(5, i);
  console.log(`(${d1}, ${d2}, ${d3})`);
}

Thực hiện mã nguồn với 1 ví dụ cụ thể

Input:

  • Số đĩa n = 4, vị thần đã di chuyển k = 10 lần.

Khởi tạo:

  • Tc = T(4) = 24 - 1 = 15
  • Tp = T(3) = 7
  • d = [4, 0, 0]
  • id = [0, 1, 2]

Lần lặp 1:

  • n = 4, k = 10, id = [0, 1, 2], d = [4, 0, 0], Tp = 7 < k = 10 < Tc
  • Từ bước 0 đến bước 7 (T(n-1)): di chuyển 3 đĩa từ cột 0 sang cột 1 với cột 2 là trung gian, d từ [4, 0, 0] thành [1, 3, 0]
  • Bước 8 (T(n-1) + 1): di chuyển 1 đĩa từ cột 0 sang cột 2, d từ [1, 3, 0] thành [0, 3, 1]
    • Hoán đổi id từ [0, 1, 2] thành [1, 0, 2].
    • Giảm k = 10 - 8 = 2
    • Giảm Tc = 7, Tp = 3
    • Giảm n = 3
  • Kết thúc vòng lặp, chuyển sang bài toán nhỏ hơn:
    • n = 3, k = 2, Tc = T(3) = 7, Tp = T(2) = 3, di chuyển 2 đĩa từ cột 1 sang cột 2 với cột 0 trung gian.

Lần lặp 2:

  • n = 3, k = 2, id = [1, 0, 2], d = [0, 3, 1], k = 2 < Tp = 3.
  • Giảm Tc = 3, Tp = 1, n = 2
  • id đổi từ [1, 0, 2] thành [1, 2, 0]
  • Kết thúc vòng lặp, chuyển sang bài toán nhỏ hơn:
    • n = 2, k = 2, Tc = T(2) = 3, Tp = T(1) = 1, di chuyển 2 đĩa từ cột 1 sang cột 0 với cột 2 làm trung gian.

Lần lặp 3:

  • n = 2, k = 2, id = [1, 2, 0], d = [0, 3, 1], k = 2 >= Tp + 1
  • Bước 0 tới bước 1, di chuyển 1 đĩa từ cột 1 sang cột 2, d từ [0, 3, 1] thành [0, 2, 2].
  • Bước 1 tới bước 2, di chuyển 1 đĩa từ cột 1 sang cột 0, d từ [0, 2, 2] thành [1, 1, 2].

Thoát khỏi vòng lặp:

  • d = [1, 1, 2]

Complexity

Bước khởi tạo:

  • Khởi tạo biến: O(1)
  • Math.pow(2, n): O(n) hoặc O(logn)

Vòng lặp, mỗi lần lặp giảm n một giá trị. n -> 0 thì k -> 0 (vì 0 <= k < 2n). Các lệnh trong vòng lặp là O(1) => O(n)

Tóm lại, O(n)

6 Likes

viết dài quá chắc là đúng :joy:

O(k) nha, n là số đĩa, k lên tới 2n lận mà, sao O(n) được (chưa đọc hết ko biết có phải O(n) hay ko :joy:)

edit: chắc là O(n) thật :joy:

1 Like

Phân tích ban đầu là với k, 0 <= k < 2n. Sau đó tìm ra số m sao cho 2m < k < 2m+1, m < n.

Nếu n lớn hơn nhiều so với m thì khi lặp nó chỉ chạy đoạn else đến khi n = m+1.
Ví dụ như n = 10, m = 5, trong elsek -= Tp+1 hay “k -= 2n-1”.

  • n = 10, k bị trừ 29, n = 9 k bị trừ 28,…, n = 7 k bị trừ 26
  • n = 6 thì bắt đầu xét khoảng [25-1, 26-1], lúc này mấy điều kiện khác của if thoả mãn.

Oops, n lớn hơn nhiều m thì đoạn code của k < Tp thực hiện liên tục từ n đến m+2.
Khi n đến m+1, tương ứng khoảng T(m) và T(m+1) mới thực thi các case khác.

Cũng vì mỗi if đoạn không trừ k, đoạn đặt k = 0 luôn, đoạn trừ luôn 1 nửa giá trị T(n) (hay T(n-1)), nên tính theo k hơi khó :sweat_smile:

Vì vậy chuyển sang tính n cho tiện :yum:. Mỗi lần xong 1 vòng lặp là được 1 bài toán con tương tự nhưng với n và k nhỏ hơn, thoả mãn 0 <= k < 2n.

3 Likes

mỗi lần xong 1 vòng lặp là n nhỏ hơn 1, tổng số lần duy chuyển nhỏ hơn 2 lần nên tuy k ~ 2n nhưng chỉ lặp logk lần ~ n lần vậy O(n) là đúng

minh họa :joy:

n = 5, k chạy từ 0 tới 31 hay 0 tới 2n - 1 nghĩa là k là số nguyên 5 bit (n bit): 00000 tới 11111

hình 3 tháp trong 32 lần di chuyển:

54321 ----- -----
5432- ----- 1----
543-- 2---- 1----
543-- 21--- -----
54--- 21--- 3----
541-- 2---- 3----
541-- ----- 32---
54--- ----- 321--
5---- 4---- 321--
5---- 41--- 32---
52--- 41--- 3----
521-- 4---- 3----
521-- 43--- -----
52--- 43--- 1----
5---- 432-- 1----
5---- 4321- -----
----- 4321- 5----
1---- 432-- 5----
1---- 43--- 52---
----- 43--- 521--
3---- 4---- 521--
3---- 41--- 52---
32--- 41--- 5----
321-- 4---- 5----
321-- ----- 54---
32--- ----- 541--
3---- 2---- 541--
3---- 21--- 54---
----- 21--- 543--
1---- 2---- 543--
1---- ----- 5432-
----- ----- 54321

có thể chia thành 5 đoạn:

k = 00000
54321 ----- -----

k = 00001
5432- ----- 1----

k = 0001x
543-- 2---- 1----
543-- 21--- -----

k = 001xy
54--- 21--- 3----
541-- 2---- 3----
541-- ----- 32---
54--- ----- 321--

k = 01xyz
5---- 4---- 321--
5---- 41--- 32---
52--- 41--- 3----
521-- 4---- 3----
521-- 43--- -----
52--- 43--- 1----
5---- 432-- 1----
5---- 4321- -----

k = 1xyzw
----- 4321- 5----
1---- 432-- 5----
1---- 43--- 52---
----- 43--- 521--
3---- 4---- 521--
3---- 41--- 52---
32--- 41--- 5----
321-- 4---- 5----
321-- ----- 54---
32--- ----- 541--
3---- 2---- 541--
3---- 21--- 54---
----- 21--- 543--
1---- 2---- 543--
1---- ----- 5432-
----- ----- 54321

chia được vậy rồi tìm “nhị phân” là O(logk) --> O(n) :V

3 Likes

Billion thanks, you guys are the true hero :smiley:

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?