Bài toán phân luồn cực đại dùng để làm gì năm 2024

Ta gọi mạng (network) là một đồ thị có hướng G = (V, E), trong đó có duy nhất một đỉnh A không có cung đi vào gọi là điểm phát (source), duy nhất một đỉnh B không có cung đi ra gọi là đỉnh thu (sink) và mỗi cung e = (u, v) ∈ E được gán với một số không âm c(e) = c[u, v] gọi là khả năng thông qua của cung đó (capacity). Để thuận tiện cho việc trình bày, ta qui ước rằng nếu không có cung (u, v) thì khả năng thông qua c[u, v] của nó được gán bằng 0.

Nếu có mạng G = (V, E). Ta gọi luồng (flow) f trong mạng G là một phép gán cho mỗi cung e = (u, v) ∈ E một số thực không âm f(e) = f[u, v] gọi là luồng trên cung e, thoả mãn các điều kiện sau:

Luồng trên mỗi cung không vượt quá khả năng thông qua của nó: 0 ≤ f[u, v] ≤ c[u, v] (∀ (u, v) ∈ E).

Với mọi đỉnh v không trùng với đỉnh phát A và đỉnh thu B, tổng luồng trên các cung đi vào v bằng tổng luồng trên các cung đi ra khỏi v:

Bài toán phân luồn cực đại dùng để làm gì năm 2024

Trong đó:

Γ-(v) = {u∈V⏐(u, v) ∈ E}

Γ+(v) = {w∈V⏐(v, w) ∈ E}

Giá trị của một luồng là tổng luồng trên các cung đi ra khỏi đỉnh phát = tổng luồng trên các cung đi vào đỉnh thu.

Bài toán phân luồn cực đại dùng để làm gì năm 2024
Mạng với các khả năng thông qua (1 phát, 6 thu) và một luồng của nó với giá trị 7

10.1. Bài toán

Cho mạng G = (V, E). Hãy tìm luồng f* trong mạng với giá trị luồng lớn nhất. Luồng như vậy gọi là luồng cực đại trong mạng và bài toán này gọi là bài toán tìm luồng cực đại trên mạng.

10.2. Lát cắt, đường tăng luồng, định lý Ford - Fulkerson

10.2.1. Định nghĩa:

Ta gọi lát cắt (X, Y) là một cách phân hoạch tập đỉnh V của mạng thành hai tập rời nhau X và Y, trong đó X chứa đỉnh phát và Y chứa đỉnh thu. Khả năng thông qua của lát cắt (X, Y) là tổng tất cả các khả năng thông qua của các cung (u, v) có u ∈ X và v ∈ Y. Lát cắt với khả năng thông qua nhỏ nhất gọi là lát cắt hẹp nhất.

10.2.2. Định lý Ford-Fulkerson:

Giá trị luồng cực đại trên mạng đúng bằng khả năng thông qua của lát cắt hẹp nhất. Việc chứng minh định lý Ford- Fulkerson đã xây dựng được một thuật toán tìm luồng cực đại trên mạng:

Giả sử f là một luồng trong mạng G = (V, E). Từ mạng G = (V, E) ta xây dựng đồ thị có trọng số Gf = (V, Ef) như sau:

Xét những cạnh e = (u, v) ∈ E (c[u, v] > 0):

Nếu f[u, v] < c[u, v] thì ta thêm cung (u, v) vào Ef với trọng số c[u, v] - f[u, v], cung đó gọi là cung thuận. Về ý nghĩa, trọng số cung này cho biết còn có thể tăng luồng f trên cung (u, v) một lượng không quá trọng số đó.

Xét tiếp nếu như f[u, v] > 0 thì ta thêm cung (v, u) vào Ef với trọng số f[u, v], cung đó gọi là cung nghịch. Về ý nghĩa, trọng số cung này cho biết còn có thể giảm luồng f trên cung (u, v) một lượng không quá trọng số đó.

Đồ thị Gf được gọi là đồ thị tăng luồng.

Bài toán phân luồn cực đại dùng để làm gì năm 2024
Mạng G, luồng trên các cung (1 phát, 6 thu) và đồ thị tăng luồng tương ứng

Giả sử P là một đường đi cơ bản từ đỉnh phát A tới đỉnh thu B. Gọi ∆ là giá trị nhỏ nhất của các trọng số của các cung trên đường đi P. Ta sẽ tăng giá trị của luồng f bằng cách đặt:

• f[u, v] := f[u, v] + ∆, nếu (u, v) là cung trong đường P và là cung thuận f[v, u] := f[v, u] - ∆, nếu (u, v) là cung trong đường P và là cung nghịch.

Còn luồng trên những cung khác giữ nguyên.

Có thể kiểm tra luồng f mới xây dựng vẫn là luồng trong mạng và giá trị của luồng f mới được tăng thêm ∆ so với giá trị luồng f cũ. Ta gọi thao tác biến đổi luồng như vậy là tăng luồng dọc đường P, đường đi cơ bản P từ A tới B được gọi là đường tăng luồng.

Ví dụ: với đồ thị tăng luồng Gf như trên, giả sử chọn đường đi (1, 3, 4, 2, 5, 6). Giá trị nhỏ nhất của trọng số trên các cung là 2, vậy thì ta sẽ tăng các giá trị f[1, 3]), f[3, 4], f[2, 5], f[5, 6] lên 2, (do các cung đó là cung thuận) và giảm giá trị f[2, 4] đi 2 (do cung (4, 2) là cung nghịch). Được luồng mới mang giá trị 9.

Bài toán phân luồn cực đại dùng để làm gì năm 2024
Luồng trên mạng G trước và sau khi tăng

Đến đây ta có thể hình dung ra được thuật toán tìm luồng cực đại trên mạng: khởi tạo một luồng bất kỳ, sau đó cứ tăng luồng dọc theo đường tăng luồng, cho tới khi không tìm được đường tăng luồng nữa.

Vậy các bước của thuật toán tìm luồng cực đại trên mạng có thể mô tả như sau:

Bước 1: Khởi tạo:

Một luồng bất kỳ trên mạng, chẳng hạn như luồng 0 (luồng trên các cung đều bằng 0), sau đó:

Bước 2: Lặp hai bước sau:

Tìm đường tăng luồng P đối với luồng hiện có ≡ Tìm đường đi cơ bản từ A tới B trên đồ thị tăng luồng, nếu không tìm được đường tăng luồng thì bước lặp kết thúc.

Tăng luồng dọc theo đường P

Bước 3: Thông báo giá trị luồng cực đại tìm được.

10.3. Cài đặt

Input: file văn bản MAXFLOW.INP. Trong đó:

• Dòng 1: Chứa số đỉnh n ( ≤ 100), số cạnh m của đồ thị, đỉnh phát A, đỉnh thu B theo đúng thứ tự cách nhau ít nhất một dấu cách.

• m dòng tiếp theo, mỗi dòng có dạng ba số u, v, c[u, v] cách nhau ít nhất một dấu cách thể hiện có cung (u, v) trong mạng và khả năng thông qua của cung đó là c[u, v] (c[u, v] là số nguyên dương không quá 100)

Output: file văn bản MAXFLOW.OUT, ghi luồng trên các cung và giá trị luồng cực đại tìm được.

Bài toán phân luồn cực đại dùng để làm gì năm 2024

Chú ý rằng tại mỗi bước có nhiều phương án chọn đường tăng luồng, hai cách chọn khác nhau có thể cho hai luồng cực đại khác nhau nhưng về mặt giá trị thì tất cả các luồng xây dựng được theo cách trên sẽ có cùng giá trị cực đại.

Cài đặt chương trình tìm luồng cực đại dưới đây rất chân phương, từ ma trận những khả năng thông qua c và luồng f hiện có (khởi tạo f là luồng 0), nó xây dựng đồ thị tăng luồng Gf bằng cách xây dựng ma trận cf như sau:

cf[u, v] = trọng số cung (u, v) trên đồ thị Gf nếu như (u, v) là cung thuận.

cf[u, v] = - trọng số cung (u, v) trên đồ thị Gf nếu như (u, v) là cung nghịch.

cf[u, v] = +∞ nếu như (u, v) không phải cung của Gf

cf gần giống như ma trận trọng số của Gf, chỉ có điều ta đổi dấu trọng số nếu như gặp cung nghịch.

Câu hỏi đặt ra là nếu như mạng đã cho có những đường hai chiều (có cả cung (u, v) và cung (v, u) - điều này xảy ra rất nhiều trong mạng lưới giao thông) thì đồ thị tăng luồng rất có thể là đa đồ thị (giữa u, v có thể có nhiều cung từ u tới v). Ma trận cf cũng gặp nhược điểm như ma trận trọng số:

Không thể biểu diễn được đa đồ thị, tức là nếu như có nhiều cung nối từ u tới v trong đồ thị tăng luồng thì ta đành chấp nhận bỏ bớt mà chỉ giữ lại một cung. Rất may cho chúng ta là điều đó không làm sai lệch đi mục đích xây dựng đồ thị tăng luồng: chỉ là tìm một đường đi từ đỉnh phát A tới đỉnh thu B mà thôi, còn đường nào thì không quan trọng.

Sau đó chương trình tìm đường đi từ đỉnh phát A tới đỉnh thu B trên đồ thị tăng luồng bằng thuật toán tìm kiếm theo chiều rộng, nếu tìm được đường đi thì sẽ tăng luồng dọc theo đường tăng luồng...

P_4_10_1.PAS * Thuật toán tìm luồng cực đại trên mạng program Max_Flow; const InputFile = 'MAXFLOW.INP'; OutputFile = 'MAXFLOW.OUT'; max = 100; maxC = 10000; var c, f, cf: array[1..max, 1..max] of Integer; {c: khả năng thông, f: Luồng} Trace: array[1..max] of Integer; n, A, B: Integer; procedure Enter; {Nhập mạng} var m, i, u, v: Integer; fi: Text; begin Assign(fi, InputFile); Reset(fi); FillChar(c, SizeOf(c), 0); ReadLn(fi, n, m, A, B); for i := 1 to m do

ReadLn(fi, u, v, c[u, v]);  
Close(fi); end; procedure CreateGf; {Tìm đồ thị tăng luồng, tức là xây dựng cf từ c và f} var u, v: Integer; begin for u := 1 to n do
for v := 1 to n do cf[u, v] := maxC;  
for u := 1 to n do
for v := 1 to n do  
  if c[u, v] > 0 then {Nếu u, v là cung trong mạng}  
  begin  
    if f[u, v] < c[u, v] then cf[u, v] := c[u, v] - f[u, v]; {Đặt cung thuận}  
    if f[u, v] > 0 then cf[v, u] := -f[u, v]; {Đặt cung nghịch}  
  end;  
end; {Thủ tục này tìm một đường đi từ A tới B bằng BFS, trả về TRUE nếu có đường, FALSE nếu không có đường} function FindPath: Boolean; var Queue: array[1..max] of Integer; {Hàng đợi dùng cho BFS} Free: array[1..max] of Boolean; u, v, First, Last: Integer; begin FillChar(Free, SizeOf(Free), True); First := 1; Last := 1; Queue[1] := A; {Queue chỉ gồm một đỉnh phát A} Free[A] := False; {đánh dấu A} repeat
u := Queue[First];  
Inc(First); {Lấy u khỏi Queue}  
for v := 1 to n do  
  if Free[v] and (cf[u, v] <> maxC) then {Xét v chưa đánh dấu kề với u}  
  begin  
    Trace[v] := u; {Lưu vết đường đi A → ... → u → v}  
    if v = B then {v = B thì ta có đường đi từ A tới B, thoát thủ tục}  
    begin  
      FindPath := True;  
      Exit;  
    end;  
    Free[v] := False; {đánh dấu v}  
    Inc(Last);  
    Queue[Last] := v; {Queue ← v}  
  end;  
until First > Last; {Queue rỗng} FindPath := False; {ở trên không Exit được thì tức là không có đường} end; {Thủ tục tăng luồng dọc theo đường tăng luồng tìm được trong FindPath} procedure IncFlow; var u, v, IncValue: Integer; begin {Trước hết dò đường theo vết để tìm trọng số nhỏ nhất của các cung trên đường} IncValue := maxC; v := B; while v <> A do begin
u := Trace[v]; {Để ý rằng ⏐cf[u, v]⏐ là trọng số của cung (u, v) trên đồ thị tăng luồng}  
if Abs(cf[u, v]) < IncValue then IncValue := Abs(cf[u, v]);  
v:= u;  
end; {Dò lại đường lần thứ hai, lần này để tăng luồng} v := B; while v <> A do begin
u := Trace[v];  
if cf[u, v] > 0 then f[u, v] := f[u, v] + IncValue {Nếu (u, v) là cung thuận trên Gf}  
else f[v, u] := f[v, u] - IncValue; {Nếu (u, v) là cung nghịch trên Gf}  
v := u;  
end; end; procedure PrintResult; {In luồng cực đại tìm được} var u, v, m: Integer; fo: Text; begin Assign(fo, OutputFile); Rewrite(fo); m := 0; for u := 1 to n do
for v := 1 to n do  
  if c[u, v] > 0 then {Nếu có cung (u, v) trên mạng thì in ra giá trị luồng f gán cho cung đó}  
  begin  
    WriteLn(fo, 'f(', u, ', ', v, ') = ', f[u, v]);  
    if u = A then m := m + f[A, v]; {Giá trị luồng cực đại = tổng luồng phát ra từ A}  
  end;  
WriteLn(fo, 'Max Flow: ', m); Close(fo); end; begin Enter; {Nhập dữ liệu} FillChar(f, SizeOf(f), 0); {Khởi tạo luồng 0} repeat {Bước lặp}
CreateGf; {Dựng đồ thị tăng luồng}  
if not FindPath then Break; {Nếu không tìm được đường tăng luồng thì thoát ngay}  
IncFlow; {Tăng luồng dọc đường tăng luồng}  
until False; PrintResult; end.

Bây giờ ta thử xem cách làm trên được ở chỗ nào và chưa hay ở chỗ nào?

Trước hết, thuật toán tìm đường bằng Breadth First Search là khá tốt, người ta đã chứng minh rằng nếu như đường tăng luồng được tìm bằng BFS sẽ làm giảm đáng kể số bước lặp tăng luồng so với DFS.

Nhưng có thể thấy rằng việc xây dựng tường minh cả đồ thị Gf thông qua việc xây dựng ma trận cf chỉ để làm mỗi một việc tìm đường là lãng phí, chỉ cần dựa vào ma trận khả năng thông qua c và luồng f hiện có là ta có thể biết được (u, v) có phải là cung trên đồ thị tăng luồng Gf hay không.

Thứ hai, tại bước tăng luồng, ta phải dò lại hai lần đường đi, một lần để tìm trọng số nhỏ nhất của các cung trên đường, một lần để tăng luồng. Trong khi việc tìm trọng số nhỏ nhất của các cung trên đường có thể kết hợp làm ngay trong thủ tục tìm đường bằng cách sau:

Đặt Delta[v] là trọng số nhỏ nhất của các cung trên đường đi từ A tới v, khởi tạo Delta[A] = +∞.

Tại mỗi bước từ đỉnh u thăm đỉnh v trong BFS, thì Delta[v] có thể được tính bằng giá trị nhỏ nhất trong hai giá trị Delta[u] và trọng số cung (u, v) trên đồ thị tăng luồng. Khi tìm được đường đi từ A tới B thì Delta[B] cho ta trọng số nhỏ nhất của các cung trên đường tăng luồng.

Thứ ba, ngay trong bước tìm đường tăng luồng, ta có thể xác định ngay cung nào là cung thuận, cung nào là cung nghịch. Vì vậy khi từ đỉnh u thăm đỉnh v trong BFS, ta có thể vẫn lưu vết đường đi Trace[v] := u, nhưng sau đó sẽ đổi dấu Trace[v] nếu như (u, v) là cung nghịch.

Những cải tiến đó cho ta một cách cài đặt hiệu quả hơn, đó là:

10.4. Thuật toán Ford - Fulkerson (L.R.FORD & D.R.FULKERSON - 1962)

Mỗi đỉnh v được gán nhãn (Trace[v], Delta[v]). Trong đó ⏐Trace[v]⏐ là đỉnh liền trước v trong đường đi từ A tới v, Trace[v] âm hay dương tuỳ theo (⏐Trace[v]⏐, v) là cung nghịch hay cung thuận trên đồ thị tăng luồng, Delta[v] là trọng số nhỏ nhất của các cung trên đường đi từ A tới v trên đồ thị tăng luồng.

Bước lặp sẽ tìm đường đi từ A tới B trên đồ thị tăng luồng đồng thời tính luôn các nhãn (Trace[v], Delta[v]). Sau đó tăng luồng dọc theo đường tăng luồng nếu tìm thấy.

P_4_10_2.PAS * Thuật toán Ford-Fulkerson program Max_Flow_by_Ford_Fulkerson; const InputFile = 'MAXFLOW.INP'; OutputFile = 'MAXFLOW.OUT'; max = 100; maxC = 10000; var c, f: array[1..max, 1..max] of Integer; Trace: array[1..max] of Integer; Delta: array[1..max] of Integer; n, A, B: Integer; procedure Enter; {Nhập dữ liệu} var m, i, u, v: Integer; fi: Text; begin Assign(fi, InputFile); Reset(fi); FillChar(c, SizeOf(c), 0); ReadLn(fi, n, m, A, B); for i := 1 to m do

ReadLn(fi, u, v, c[u, v]);  
Close(fi); end; function Min(X, Y: Integer): Integer; begin if X < Y then Min := X else Min := Y; end; function FindPath: Boolean; var u, v: Integer; Queue: array[1..max] of Integer; First, Last: Integer; begin FillChar(Trace, SizeOf(Trace), 0); {Trace[v] = 0 đồng nghĩa với v chưa đánh dấu} First := 1; Last := 1; Queue[1] := A; Trace[A] := n + 1; {Chỉ cần nó khác 0 để đánh dấu mà thôi, số dương nào cũng được cả} Delta[A] := maxC; {Khởi tạo nhãn} repeat
u := Queue[First];  
Inc(First); {Lấy u khỏi Queue}  
for v := 1 to n do  
  if Trace[v] = 0 then {Xét nhứng đỉnh v chưa đánh dấu thăm}  
  begin  
    if f[u, v] < c[u, v] then {Nếu (u, v) là cung thuận trên Gf và có trọng số là c[u, v] - f[u, v]}  
    begin  
      Trace[v] := u; {Lưu vết, Trace[v] mang dấu dương}  
      Delta[v] := min(Delta[u], c[u, v] - f[u, v]);  
    end  
    else  
      if f[v, u] > 0 then {Nếu (u, v) là cung nghịch trên Gf và có trọng số là f[v, u]}  
      begin  
        Trace[v] := -u; {Lưu vết, Trace[v] mang dấu âm}  
        Delta[v] := min(Delta[u], f[v, u]);  
      end;  
    if Trace[v] <> 0 then {Trace[v] khác 0 tức là từ u có thể thăm v}  
    begin  
      if v = B then {Có đường tăng luồng từ A tới B}  
      begin  
        FindPath := True; Exit;  
      end;  
      Inc(Last);  
      Queue[Last] := v; {Đưa v vào Queue}  
    end;  
  end;  
until First > Last; {Hàng đợi Queue rỗng} FindPath := False; {ở trên không Exit được tức là không có đường} end; procedure IncFlow; {Tăng luồng dọc đường tăng luồng} var IncValue, u, v: Integer; begin IncValue := Delta[B]; {Nhãn Delta[B] chính là trọng số nhỏ nhất trên các cung của đường tăng luồng} v := B; {Truy vết đường đi, tăng luồng dọc theo đường đi} repeat
u := Trace[v]; {Xét cung (⏐u⏐, v) trên đường tăng luồng}  
if u > 0 then f[u, v] := f[u, v] + IncValue {(|u|, v) là cung thuận thì tăng f[u, v]}  
else  
begin  
  u := -u;  
  f[v, u] := f[v, u] - IncValue; {(|u|, v) là cung nghịch thì giảm f[v, |u|]}  
end;  
v := u;  
until v = A; end; procedure PrintResult; {In kết quả} var u, v, m: Integer; fo: Text; begin Assign(fo, OutputFile); Rewrite(fo); m := 0; for u := 1 to n do
for v := 1 to n do  
  if c[u, v] > 0 then  
  begin  
    WriteLn(fo, 'f(', u, ', ', v, ') = ', f[u, v]);  
    if u = A then m := m + f[A, v];  
  end;  
WriteLn(fo, 'Max Flow: ', m); Close(fo); end; begin Enter; FillChar(f, SizeOf(f), 0); repeat
if not FindPath then Break;  
IncFlow;  
until False; PrintResult; end.

Định lý về luồng cực đại trong mạng và lát cắt hẹp nhất:

Luồng cực đại trong mạng bằng khả năng thông qua của lát cắt hẹp nhất. Khi đã tìm được luồng cực đại thì theo thuật toán trên sẽ không có đường đi từ A tới B trên đồ thị tăng luồng. Nếu đặt tập X gồm những đỉnh đến được từ đỉnh phát A trên đồ thị tăng luồng (tất nhiên A∈X) và tập Y gồm những đỉnh còn lại (tất nhiên B∈Y) thì (X, Y) là lát cắt hẹp nhất đó. Có thể có nhiều lát cắt hẹp nhất, ví dụ nếu đặt tập Y gồm những đỉnh đến được đỉnh thu B trên đồ thị tăng luồng (tất nhiên B∈ Y) và tập X gồm những đỉnh còn lại thì (X, Y) cũng là một lát cắt hẹp nhất.

Định lý về tính nguyên:

Nếu tất cả các khả năng thông qua là số nguyên thì thuật toán trên luôn tìm được luồng cực đại với luồng trên cung là các số nguyên. Điều này có thể chứng minh rất dễ bởi ban đầu khởi tạo luồng 0 thì tức các luồng trên cung là nguyên. Mỗi lần tăng luồng lên một lượng bằng trọng số nhỏ nhất trên các cung của đường tăng luồng cũng là số nguyên nên cuối cùng luồng cực đại tất sẽ phải có luồng trên các cung là nguyên.

Định lý về chi phí thời gian thực hiện giải thuật:

Trong phương pháp Ford-Fulkerson, nếu dùng đường đi ngắn nhất (qua ít cạnh nhất) từ đỉnh phát tới đỉnh thu trên đồ thị tăng luồng thì cần ít hơn n.m lần chọn đường đi để tìm ra luồng cực đại.

Edmonds và Karp đã chứng minh tính chất này và đề nghị một phương pháp cải tiến: Tại mỗi bước, ta nên tìm đường tăng luồng sao cho giá trị tăng luồng được gia tăng nhiều nhất.

Nói chung đối với thuật toán Ford-Fulkerson, các đánh giá lý thuyết bị lệch rất nhiều so với thực tế, mặc dù với sự phân tích trong trường hợp xấu, chi phí thời gian thực hiện của thuật toán là khá lớn.

Nhưng trên thực tế thì thuật toán này hoạt động rất nhanh và hiệu quả.

Bài tập:

Bài 1

Mạng với nhiều điểm phát và nhiều điểm thu: Cho một mạng gồm n đỉnh với p điểm phát A1, A2, ..., Ap và q điểm thu B1, B2, ..., Bq. Mỗi cung của mạng được gán khả năng thông qua là số nguyên. Các đỉnh phát chỉ có cung đi ra và các đỉnh thu chỉ có cung đi vào. Một luồng trên mạng này là một phép gán cho mỗi cung một số thực gọi là luồng trên cung đó không vượt quá khả năng thông qua và thoả mãn với mỗi đỉnh không phải đỉnh phát hay đỉnh thu thì tổng luồng đi vào bằng tổng luồng đi ra. Giá trị luồng bằng tổng luồng đi ra từ các đỉnh phát = tổng luồng đi vào các đỉnh thu. Hãy tìm luồng cực đại trên mạng.

Bài 2

Mạng với khả năng thông qua của các đỉnh và các cung: Cho một mạng với đỉnh phát A và đỉnh thu B. Mỗi cung (u, v) được gán khả năng thông qua c[u, v]. Mỗi đỉnh v khác với A và B được gán Các thuật toán trên đồ thị khả năng thông qua d[v]. Một luồng trên mạng được định nghĩa như trước và thêm điều kiện: tổng luồng đi vào đỉnh v không được vượt quá khả năng thông qua d[v] của đỉnh đó. Hãy tìm luồng cực đại trên mạng.

Bài 3

Lát cắt hẹp nhất: Cho một đồ thị gồm n đỉnh và m cạnh và 2 đỉnh A, B. Hãy tìm cách bỏ đi một số ít nhất các cạnh để không còn đường đi từ A tới B.

Bài 4

Một lớp học có n bạn nam, n bạn nữ. Cho m món quà lưu niệm, (n ≤ m). Mỗi bạn có sở thích về một số món quà nào đó. Hãy tìm cách phân cho mỗi bạn nam tặng một món quà cho một bạn nữ thoả mãn:

Mỗi bạn nam chỉ tặng quà cho đúng một bạn nữ

Mỗi bạn nữ chỉ nhận quà của đúng một bạn nam

Bạn nam nào cũng đi tặng quà và bạn nữ nào cũng được nhận quà, món quà đó phải hợp sở thích của cả hai người.