Chương 9·Bài 9.2

Tìm kiếm và suy luận

Nếu biết đầy đủ bản đồ và thời gian đi từng đoạn hành lang, robot ở bài trước không cần học: chỉ cần tìm đường như trong lập trình thi đấu. Bài này học ba loại bài toán tìm kiếm và suy luận trong AI: tìm đường rẻ nhất (A*), chơi trò chơi có đối thủ (minimax), và kiểm một kết luận có chắc chắn đúng từ các điều kiện đã biết (CSP, logic mệnh đề). Đây cũng là AI, dù không học từ dữ liệu.

Trạng thái đúng trước, thuật toán sau

Một bài toán tìm kiếm có trạng thái đầu, các hành động hợp lệ, quy tắc sinh trạng thái mới, phép kiểm đích và chi phí.

Nếu hai lịch sử tới cùng vị trí dẫn tới khả năng hành động giống nhau, vị trí có thể làm trạng thái. Nếu robot phải nhặt chìa khóa mới mở được cửa, cần thêm thông tin đã có chìa hay chưa. Đánh dấu visited[position] lúc ấy sẽ gộp hai tình huống khác nhau và có thể xóa mất đáp án.

Đồ thị của ta có bốn đỉnh. Từ S có thể tới B với giá 1 hoặc A với giá 2; từ B tới G giá 10, từ A tới G giá 2. Cạnh có hướng, chi phí tính bằng giây, mọi chi phí dương.

BFS đếm cạnh nên chọn đường 11 giây thay vì đường 4 giây.12210SABGS-A-G: 4 giâyS-B-G: 11 giâycả hai đường đều 2 cạnh; BFS trả đường qua B
BFS đếm cạnh nên chọn đường 11 giây thay vì đường 4 giây.

BFS mở rộng theo số cạnh nên tối ưu khi mọi cạnh có cùng chi phí. Dijkstra mở rộng theo chi phí đã đi g(n)g(n) và cần cạnh không âm. A* thêm một ước lượng chi phí còn lại h(n)h(n), mở rộng theo

f(n)=g(n)+h(n).f(n)=g(n)+h(n).

Ở đây gg, hh, ff đều có đơn vị giây. Cộng số bước với số giây không có nghĩa nếu chưa quy đổi. Greedy best-first search chỉ dùng hh; nó có thể lao tới một đỉnh trông gần đích dù đã đi qua đoạn rất đắt. A* giữ cả chi phí đã trả.

Ta để B đứng trước A trong danh sách cạnh. Hai đường đều có 2 cạnh, nên BFS trả đường qua B; đây là lời giải ít cạnh nhất nhưng không ít giây nhất. Code dưới đây cài BFS trên đúng đồ thị này rồi tính số giây của đường nó trả về.

Python
from collections import deque
import heapq
import math

graph = {
    "S": [("B", 1), ("A", 2)],
    "A": [("G", 2)],
    "B": [("G", 10)],
    "G": [],
}

def path_cost(path):
    total = 0
    for source, target in zip(path, path[1:]):
        total += next(cost for node, cost in graph[source] if node == target)
    return total

def bfs_path(source, target):
    queue = deque([[source]])
    seen = {source}
    while queue:
        path = queue.popleft()
        if path[-1] == target:
            return path
        for node, _ in graph[path[-1]]:
            if node not in seen:
                seen.add(node)
                queue.append(path + [node])
    return None

bfs_answer = bfs_path("S", "G")
print("BFS:", bfs_answer, "chi phí:", path_cost(bfs_answer))

Đường dài 11 giây không phải lỗi của BFS; ta đã hỏi nó sai đại lượng. Nếu mọi cạnh là 1 giây thì tối thiểu số cạnh cũng là tối thiểu thời gian.

Hỏi nhanh

Robot tới cùng một ô hai lần: một lần đã nhặt chìa khóa, một lần chưa. Có nên gộp hai lần đó thành một trạng thái không?

Hỏi nhanh

Đồ thị có cạnh 1 giây và cạnh 2 giây. Một bạn muốn giữ BFS. Sửa thế nào để BFS cho ra đúng đường ít giây nhất?

DFS và IDS: khi bộ nhớ là giới hạn

BFS lấy nút ra từ hàng đợi (queue) nên mở theo từng tầng. DFS lấy từ ngăn xếp (stack), đi hết một nhánh rồi mới quay lui. Hai cách khác nhau ở thứ tự mở, kéo theo khác nhau về bộ nhớ và về chất lượng lời giải.

Cùng một cây: BFS thăm theo từng tầng, DFS đi hết nhánh trái rồi mới quay lui.1245367BFShàng đợi: thăm từng tầng1234567DFSngăn xếp: hết nhánh mới quay lui
Cùng một cây: BFS thăm theo từng tầng, DFS đi hết nhánh trái rồi mới quay lui.

Trên cây có hệ số nhánh bb và lời giải nông nhất ở độ sâu dd, BFS phải giữ cả tầng biên, cỡ O(bd)O(b^d) nút. DFS chỉ giữ đường đi từ gốc cùng các nút anh em chưa mở dọc đường đó, cỡ O(bm)O(bm) với mm là độ sâu lớn nhất. Với b=10b = 10, d=8d = 8, BFS có thể phải giữ 10810^8 nút, còn DFS chỉ vài chục.

Đổi lại, DFS không tối ưu: nó có thể lao xuống một nhánh sâu và mất rất lâu mới quay sang nhánh có đích nông. Trên đồ thị hữu hạn có đánh dấu đã thăm, DFS vẫn tìm được đích; trên đồ thị vô hạn, hay khi không đánh dấu mà đồ thị có vòng, nó có thể đi mãi.

Iterative deepening search (IDS) lấy ưu điểm của cả hai: chạy DFS với giới hạn độ sâu 0, 1, 2, … cho tới khi gặp đích. Bộ nhớ như DFS. Vì giới hạn tăng từng bậc nên đích được gặp ở tầng nông nhất, như BFS. Các tầng trên bị mở lại nhiều lần, nhưng tầng cuối chiếm phần lớn số nút, nên tổng công chỉ hơn BFS một hằng số. Còn uniform-cost search (UCS) trong sách AI chính là Dijkstra: mở nút có gg nhỏ nhất.

Ta đo trên cây đầy đủ có 3 nhánh mỗi nút, sâu 6, đếm số nút được lấy ra và số nút lớn nhất phải giữ cùng lúc. Một đích nằm ở lá phải cùng, một đích nằm ở độ sâu 2.

Python
BRANCH, DEPTH = 3, 6

def children(node):
    return [] if len(node) == DEPTH else [node + (k,) for k in range(BRANCH)]

def bfs_count(goal):
    queue, peak, opened = deque([()]), 1, 0
    while queue:
        node = queue.popleft()
        opened += 1
        if node == goal:
            return opened, peak
        queue.extend(children(node))
        peak = max(peak, len(queue))

def dfs_count(goal):
    stack, peak, opened = [()], 1, 0
    while stack:
        node = stack.pop()
        opened += 1
        if node == goal:
            return opened, peak
        stack.extend(reversed(children(node)))
        peak = max(peak, len(stack))

def ids_count(goal):
    opened, peak = 0, 1
    for limit in range(DEPTH + 1):
        stack = [()]
        while stack:
            node = stack.pop()
            opened += 1
            if node == goal:
                return opened, peak
            if len(node) < limit:
                stack.extend(reversed(children(node)))
                peak = max(peak, len(stack))

for goal in [(2,) * DEPTH, (1, 0)]:
    print(f"đích {goal}:", end="")
    for name, search in [("BFS", bfs_count), ("DFS", dfs_count), ("IDS", ids_count)]:
        opened, peak = search(goal)
        print(f"  {name} mở {opened}, giữ {peak}", end="")
    print()

Với đích ở lá cuối, cả BFS và DFS đều mở hết 1093 nút, nhưng BFS phải giữ cả tầng lá, 729 nút, còn DFS chỉ giữ 13. Với đích nông, DFS lạc vào nhánh 0 và mở 367 nút trước khi quay sang, còn BFS chỉ mở 8. IDS mở nhiều hơn BFS khoảng một nửa ở ca đầu, nhưng ở cả hai ca chỉ giữ ít nút như DFS.

Hỏi nhanh

Cây có hệ số nhánh b=10b = 10, đích nông nhất ở độ sâu d=6d = 6, cây sâu tới m=20m = 20, máy chỉ đủ bộ nhớ cho vài nghìn nút. Cần tìm đường ít cạnh nhất. Nên dùng gì?

A*: cần một lời hứa về heuristic

A* ưu tiên mở đỉnh có f(n)=g(n)+h(n)f(n)=g(n)+h(n) nhỏ nhất, với g(n)g(n) là chi phí đã đi và h(n)h(n) là ước lượng chi phí còn lại (heuristic). Heuristic gọi là admissible (chấp nhận được) khi không ước lượng quá chi phí thật nhỏ nhất còn lại: 0≤h(n)≤h∗(n)0\leq h(n)\leq h^*(n), với h(G)=0h(G)=0.

Trên đồ thị này, ta chọn h(S)=4h(S)=4, h(A)=2h(A)=2, h(B)=10h(B)=10, h(G)=0h(G)=0. Đó chính là chi phí còn lại thật nên chắc chắn admissible. Ở bài lớn, biết ngay h∗h^* thường khó ngang bài gốc; heuristic hữu ích phải tính rẻ hơn.

Ví dụ quen thuộc là khoảng cách Manhattan trên lưới chỉ đi bốn hướng, mỗi bước tốn ít nhất một đơn vị, không có dịch chuyển tức thời. Bỏ tường làm bài dễ hơn; đường ngắn nhất khi bỏ tường không thể đắt hơn đường có tường. Vì vậy khoảng cách ấy là một cận dưới (lower bound).

Nếu đề có cạnh dịch chuyển hoặc bước đường chéo giá rẻ, chứng minh này phải xem lại.

Hỏi nhanh

Robot đi được 8 hướng như quân vua, mỗi bước, kể cả bước chéo, tốn 1 giây, và không có tường. Heuristic nào vừa admissible vừa sát nhất?

Đi 8 hướng: Manhattan và Euclid ước lượng quá, max(|Δx|, |Δy|) đúng bằng 3.SG3 bước chéoManhattan |Δx| + |Δy|6vượtEuclid4.24vượtmax(|Δx|, |Δy|)3đúng bằngmin(|Δx|, |Δy|)3yếu khi lệch khác nhau
Đi 8 hướng: Manhattan và Euclid ước lượng quá, max(|Δx|, |Δy|) đúng bằng 3.

Một điều kiện mạnh hơn là consistency:

h(u)≤c(u,v)+h(v)với mọi cạnh u→v.h(u)\leq c(u,v)+h(v)\quad\text{với mọi cạnh }u\to v.

Heuristic consistent (nhất quán) làm ff không giảm dọc đường; khi đó A* có thể đóng một trạng thái ngay sau lần đầu lấy nó ra khỏi heap. Nếu chỉ có admissibility, ta phải cho phép mở lại trạng thái khi tìm được gg nhỏ hơn. Code dưới đây không dùng một tập closed khóa vĩnh viễn, nên vẫn xử lý cập nhật đó.

Tính tay tại S: g(S)=0g(S)=0, f(S)=4f(S)=4. Sau khi mở S, A có g=2g=2, f=4f=4; B có g=1g=1, f=11f=11. A được lấy ra trước, sinh G với g=4g=4, f=4f=4. G tiếp theo được lấy ra, cho đường 4 giây. Chỉ dừng khi lấy đích ra khỏi heap, không dừng ngay khi một cạnh đầu tiên sinh được đích.

Python
def astar(source, target, heuristic):
    best_cost = {source: 0}
    parent = {source: None}
    heap = [(heuristic[source], 0, source)]
    expanded = []
    while heap:
        _, cost, node = heapq.heappop(heap)
        if cost != best_cost[node]:       # bỏ mục heap đã lỗi thời
            continue
        expanded.append(node)
        if node == target:
            path = []
            while node is not None:
                path.append(node)
                node = parent[node]
            return cost, path[::-1], expanded
        for next_node, edge_cost in graph[node]:
            assert edge_cost >= 0
            candidate = cost + edge_cost
            if candidate < best_cost.get(next_node, math.inf):
                best_cost[next_node] = candidate
                parent[next_node] = node
                priority = candidate + heuristic[next_node]
                heapq.heappush(heap, (priority, candidate, next_node))
    return math.inf, None, expanded

h_good = {"S": 4, "A": 2, "B": 10, "G": 0}
h_zero = {node: 0 for node in graph}
h_bad = {"S": 0, "A": 100, "B": 0, "G": 0}
for name, heuristic in [("Dijkstra", h_zero), ("A*", h_good), ("A* h sai", h_bad)]:
    cost, path, expanded = astar("S", "G", heuristic)
    print(f"{name}: cost={cost}, path={path}, mở={expanded}")
Heuristic tốt giúp A* mở ít đỉnh hơn Dijkstra; heuristic ước lượng quá làm A* trả đường sai.Dijkstra, h = 0SBAGmở 4 · chi phí 4A*, h đúngSAGmở 3 · chi phí 4A*, h(A) = 100SBGmở 3 · chi phí 11thứ tự lấy đỉnh ra khỏi heap
Heuristic tốt giúp A* mở ít đỉnh hơn Dijkstra; heuristic ước lượng quá làm A* trả đường sai.

Dijkstra là A với h=0h=0. Heuristic đúng giúp tránh mở B trong ví dụ này. Heuristic sai gán 100 cho A, khiến đường qua A bị trì hoãn; G qua B được lấy ra với chi phí 11 và thuật toán dừng. Không được suy ra A luôn nhanh hơn Dijkstra: heuristic tốn thời gian, có thể yếu, và nhiều đỉnh có cùng ff làm thứ tự phá hòa ảnh hưởng số lần mở.

Vì đồ thị hữu hạn và cạnh dương, một đường ngắn nhất không cần lặp đỉnh. Ta viết oracle vét cạn mọi đường đơn. Nó chậm trên đồ thị lớn nhưng độc lập với heap và heuristic, thích hợp để kiểm ví dụ nhỏ như code trâu trong CP.

Python
def exhaustive_cost(source, target, seen=None):
    seen = {source} if seen is None else seen
    if source == target:
        return 0
    candidates = []
    for node, cost in graph[source]:
        if node not in seen:
            tail = exhaustive_cost(node, target, seen | {node})
            candidates.append(cost + tail)
    return min(candidates, default=math.inf)

truth = {node: exhaustive_cost(node, "G") for node in graph}
assert all(0 <= h_good[node] <= truth[node] for node in graph)
assert all(h_good[u] <= cost + h_good[v] for u in graph for v, cost in graph[u])
assert astar("S", "G", h_good)[0] == truth["S"] == 4
violations = [node for node in graph if h_bad[node] > truth[node]]
print("Chi phí còn lại thật:", truth)
print("Heuristic sai vượt lower bound tại:", violations)

Nhìn danh sách vi phạm: A là bằng chứng cụ thể mất admissibility. Trên bài lớn, kiểm một ít mẫu không thay thế được chứng minh lower bound cho mọi trạng thái. Trong phạm vi đồ thị nhỏ này, ta đã kiểm đủ các đỉnh và cạnh.

Hỏi nhanh

Sửa bản Dijkstra, tức h=0h = 0, của bài để dừng ngay khi sinh ra G, thay vì khi lấy G ra khỏi heap. Trên đồ thị S, A, B, G, nó trả về chi phí bao nhiêu?

Khi heuristic là một model và thời gian có hạn

Trong bài thi AI, ước lượng chi phí còn lại có khi đến từ một model: một mạng nhìn ảnh bản đồ rồi dự đoán số bước còn lại, hay một hàm đánh giá học từ các lời giải cũ. Model như vậy sai theo cả hai phía. Chỗ nó ước lượng thấp chỉ làm A* mở thêm đỉnh; chỗ nó ước lượng cao có thể đẩy lùi một đường tốt, như h(A)=100h(A)=100 ở trên.

Bảo đảm tối ưu mất, nhưng còn một cận. Nếu h(n)≤w h∗(n)h(n)\leq w\,h^*(n) ở mọi đỉnh với một hằng w≥1w\geq1, A* (bản cho mở lại như trên) trả về đường có chi phí không quá w C∗w\,C^*, trong đó C∗C^* là chi phí tối ưu. Lý do: chừng nào đích chưa được lấy ra, trên một đường tối ưu luôn có một đỉnh nn nằm trong heap với g(n)g(n) đã bằng chi phí tối ưu từ nguồn tới nn, nên

f(n)≤g(n)+w h∗(n)≤w(g(n)+h∗(n))=w C∗.f(n)\leq g(n)+w\,h^*(n)\leq w\big(g(n)+h^*(n)\big)=w\,C^*.

Đích được lấy ra trước nn, nên chi phí của nó (g=fg=f vì h=0h=0 tại đích) không vượt f(n)f(n). Weighted A* dùng chủ động điều này: lấy hh admissible rồi ưu tiên theo g+w hg+w\,h với w>1w>1. Thuật toán lao về phía đích và mở ít đỉnh hơn, đổi lại chỉ bảo đảm chi phí không quá w C∗w\,C^*.

Ta thử trên 20 bản đồ lưới 30×3030\times30, mỗi ô là tường với xác suất 0.3, đi bốn hướng, mỗi bước giá 1, từ góc trên trái tới góc dưới phải. BFS ngược từ đích cho chi phí còn lại thật h∗h^* của mọi ô. Để có một “model” với sai số biết trước, ta nhân h∗h^* của từng ô với một hệ số ngẫu nhiên trong [0.7,1.4][0.7, 1.4]: dự đoán thấp hơn thật tới 30% và cao hơn tới 40%, nên theo cận trên, đường tìm được dài không quá 1.4 lần tối ưu. Weighted A* dùng Manhattan với w=2w=2.

Python
import random

N = 30
START, GOAL = (0, 0), (N - 1, N - 1)

def neighbors(cell, walls):
    r, c = cell
    for nr, nc in [(r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)]:
        if 0 <= nr < N and 0 <= nc < N and (nr, nc) not in walls:
            yield (nr, nc)

def cost_to_goal(walls):                 # BFS ngược từ đích: h* của mọi ô
    dist = {GOAL: 0}
    queue = deque([GOAL])
    while queue:
        cell = queue.popleft()
        for nxt in neighbors(cell, walls):
            if nxt not in dist:
                dist[nxt] = dist[cell] + 1
                queue.append(nxt)
    return dist

def random_map(seed):
    rng = random.Random(seed)
    while True:                          # bốc lại tới khi START tới được GOAL
        walls = {(r, c) for r in range(N) for c in range(N) if rng.random() < 0.3}
        walls -= {START, GOAL}
        dist = cost_to_goal(walls)
        if START in dist:
            return walls, dist

def grid_astar(walls, h, weight=1):
    best = {START: 0}
    heap = [(weight * h(START), 0, START)]
    expanded = 0
    while heap:
        _, cost, cell = heapq.heappop(heap)
        if cost != best[cell]:
            continue
        expanded += 1
        if cell == GOAL:
            return cost, expanded
        for nxt in neighbors(cell, walls):
            if cost + 1 < best.get(nxt, math.inf):
                best[nxt] = cost + 1
                heapq.heappush(heap, (cost + 1 + weight * h(nxt), cost + 1, nxt))
    return math.inf, expanded

def manhattan(cell):
    return abs(cell[0] - GOAL[0]) + abs(cell[1] - GOAL[1])

results = {"Dijkstra": [], "A* Manhattan": [], "A* model": [], "Weighted A* w=2": []}
for seed in range(20):
    walls, dist = random_map(seed)
    rng = random.Random(1000 + seed)
    factor = {cell: rng.uniform(0.7, 1.4) for cell in dist}   # sai số -30% đến +40%
    def model(cell):
        return dist[cell] * factor[cell]
    setups = [("Dijkstra", lambda cell: 0, 1), ("A* Manhattan", manhattan, 1),
              ("A* model", model, 1), ("Weighted A* w=2", manhattan, 2)]
    for name, h, weight in setups:
        cost, expanded = grid_astar(walls, h, weight)
        results[name].append((cost / dist[START], expanded))

for name, rows in results.items():
    ratios = [ratio for ratio, _ in rows]
    longer = sum(ratio > 1 for ratio in ratios)
    mean_expanded = sum(expanded for _, expanded in rows) / len(rows)
    print(f"{name:15s}  dài hơn tối ưu {longer:2d}/20  tệ nhất x{max(ratios):.3f}  "
          f"mở trung bình {mean_expanded:.1f}")
assert max(r for r, _ in results["A* Manhattan"]) == 1
assert max(r for r, _ in results["A* model"]) <= 1.4
assert max(r for r, _ in results["Weighted A* w=2"]) <= 2

Đọc từng dòng:

  • Manhattan đúng cả 20 bản đồ và mở trung bình 276.9 ô, chưa tới một nửa 594.8 ô của Dijkstra.
  • Model mở 142.4 ô, khoảng một nửa Manhattan, nhưng 9 trong 20 bản đồ cho đường dài hơn tối ưu, tệ nhất 1.103 lần.
  • Weighted A* mở ít nhất, 115.0 ô, với 14 trong 20 bản đồ dài hơn, tệ nhất 1.310 lần.

Hai dòng sau đều nằm dưới cận của mình (1.4 và 2); trên các bản đồ này, đường thực tế còn tốt hơn cận khá nhiều. Muốn đáp án tối ưu chính xác thì chỉ dòng Manhattan dùng được; muốn đổi một chút chất lượng lấy tốc độ thì hai dòng sau cho biết giá phải trả. Số bản đồ bị dài hơn tối ưu còn dao động theo bản đồ được bốc; điều đáng nhớ là thứ tự: heuristic chứng minh được thì tối ưu, model và w=2w=2 mở ít đỉnh hơn nhưng có lúc sai.

A* mở ít ô hơn Dijkstra mà vẫn tối ưu; weighted A* mở ít nhất nhưng có thể trả đường dài hơn.Dijkstramở 613 · dài 58A* Manhattanmở 346 · dài 58weighted A*, w = 2mở 63 · dài 60bản đồ seed 0 của bài; đường ngắn nhất dài 58; ô đen là tường, ô cam là ô đã mở
A* mở ít ô hơn Dijkstra mà vẫn tối ưu; weighted A* mở ít nhất nhưng có thể trả đường dài hơn.
Dijkstra mở ô loang đều mọi hướng; A* mở lệch về phía đích, cùng độ dài đường đi.

Hỏi nhanh

Weighted A* dùng Manhattan với w=1.5w = 1.5, trả về một đường hợp lệ dài 54 bước, và chưa chạy thuật toán nào khác. Ta biết gì về độ dài tối ưu C∗C^*?

Hỏi nhanh

Đề yêu cầu in đúng độ dài đường ngắn nhất, sai là 0 điểm, và thời gian đủ cho Dijkstra. Theo bảng 20 bản đồ, nên dùng heuristic nào?

Khi đề giới hạn thời gian, tìm kiếm nên chạy kiểu anytime (lúc nào cũng có đáp án): tìm nhanh một lời giải hợp lệ, chẳng hạn bằng weighted A* với ww lớn, lưu lại, rồi chạy tiếp với ww nhỏ dần tới 1 nếu còn giờ; hết giờ thì nộp lời giải tốt nhất đang có.

Beam search ở bài 6.4 là một cách giới hạn ngân sách khác: mỗi độ sâu chỉ giữ kk trạng thái tốt nhất, bộ nhớ cố định, nhưng có thể loại mất lời giải tối ưu, thậm chí mọi lời giải. Trước khi chọn, đo số trạng thái mở được mỗi giây trên test lớn nhất.

Tìm kiếm cục bộ: chỉ cần một trạng thái tốt

Có bài chỉ hỏi trạng thái cuối, không hỏi đường đi: đặt 8 quân hậu không ăn nhau, xếp thời khóa biểu ít trùng tiết nhất. Tìm kiếm cục bộ (local search) giữ một trạng thái hoàn chỉnh rồi sửa dần, nên gần như không tốn bộ nhớ.

Hill climbing (leo đồi) mỗi bước chuyển sang trạng thái láng giềng tốt nhất và dừng khi không láng giềng nào tốt hơn. Chỗ dừng có thể chỉ là một cực đại địa phương (local maximum): xung quanh đều kém hơn, nhưng ở xa còn chỗ tốt hơn.

Leo đồi từ chân trái dừng ở đỉnh thấp; muốn tới đỉnh cao phải chịu đi xuống một đoạn.trạng tháikẹt ở đỉnh thấpđỉnh cao nhất
Leo đồi từ chân trái dừng ở đỉnh thấp; muốn tới đỉnh cao phải chịu đi xuống một đoạn.

Hai cách thoát hay dùng. Random restart chạy lại hill climbing từ nhiều điểm ngẫu nhiên và giữ kết quả tốt nhất. Simulated annealing (luyện kim mô phỏng) chọn một láng giềng ngẫu nhiên: tốt hơn thì nhận, xấu hơn một lượng Δ\Delta thì vẫn nhận với xác suất e−Δ/Te^{-\Delta/T}. Nhiệt độ TT giảm dần, nên lúc đầu dễ nhảy qua thung lũng, về sau gần như chỉ còn nhận bước tốt. Genetic algorithm (giải thuật di truyền) giữ cả một quần thể lời giải, chọn lọc các lời giải tốt, lai ghép và đột biến chúng để sinh thế hệ sau.

Trạng thái của bài 8 quân hậu là hàng của quân hậu ở mỗi cột; điểm là số cặp hậu ăn nhau, cần đưa về 0. Ta chạy hai cách từ cùng 100 điểm xuất phát ngẫu nhiên.

Python
N_QUEENS = 8

def attacks(rows):
    return sum(rows[i] == rows[j] or abs(rows[i] - rows[j]) == j - i
               for i in range(N_QUEENS) for j in range(i + 1, N_QUEENS))

def hill_climb(rows):
    while True:
        best, best_rows = attacks(rows), None
        for col in range(N_QUEENS):
            for row in range(N_QUEENS):
                if row != rows[col]:
                    cand = rows[:col] + [row] + rows[col + 1:]
                    if attacks(cand) < best:
                        best, best_rows = attacks(cand), cand
        if best_rows is None:            # không láng giềng nào tốt hơn
            return rows
        rows = best_rows

def annealing(rows, gen, steps=4000, t0=2.0):
    cur = attacks(rows)
    for k in range(steps):
        if cur == 0:
            break
        temperature = t0 * (1 - k / steps) + 1e-9
        col, row = gen.randrange(N_QUEENS), gen.randrange(N_QUEENS)
        cand = rows[:col] + [row] + rows[col + 1:]
        worse = attacks(cand) - cur      # dương là xấu đi
        if worse <= 0 or gen.random() < math.exp(-worse / temperature):
            rows, cur = cand, cur + worse
    return rows

gen = random.Random(0)
starts = [[gen.randrange(N_QUEENS) for _ in range(N_QUEENS)] for _ in range(100)]
climbed = sum(attacks(hill_climb(r)) == 0 for r in starts)
annealed = sum(attacks(annealing(r, random.Random(i))) == 0 for i, r in enumerate(starts))
print(f"Hill climbing giải được {climbed}/100, simulated annealing giải được {annealed}/100")

Hill climbing kẹt ở phần lớn điểm xuất phát: trạng thái còn một hai cặp hậu ăn nhau, mà dời một quân nào cũng không giảm được. Simulated annealing chấp nhận những bước làm tăng số cặp ăn nhau lúc đầu, nhờ vậy ra khỏi các chỗ kẹt đó. Với random restart, chạy lại hill climbing trung bình khoảng 7 lần là gặp một lần giải được.

Hỏi nhanh

Simulated annealing đang ở nhiệt độ T=2T = 2 và xét một bước làm điểm phạt tăng thêm 2. Xác suất nhận bước đó là bao nhiêu? Ở T=0.1T = 0.1 thì sao?

Khi người khác chọn bước tiếp theo: minimax

Tìm đường giả sử environment làm theo quy tắc; chơi cờ còn có đối thủ chọn hành động để giảm điểm của ta. Với trò chơi hai người, tất định, thông tin đầy đủ, tổng lợi ích bằng 0, minimax đánh giá theo giả thiết đối thủ cũng chơi tối ưu.

Xét cây sâu hai lượt. Ta chọn nhánh A hoặc B; sau đó đối thủ chọn một lá. Nhánh A có điểm 3 và 5, nhánh B có điểm 2 và 9; mọi điểm đo từ góc nhìn của ta. Đối thủ ở A chọn 3, ở B chọn 2. Ta nên chọn A vì max⁡(min⁡(3,5),min⁡(2,9))=3\max(\min(3,5),\min(2,9))=3.

MIN chọn 3 ở A, 2 ở B; MAX chọn A.352933A2BMAXMINlá 9 lớn nhất cây nhưng MIN ở B chọn 2
MIN chọn 3 ở A, 2 ở B; MAX chọn A.

Nếu U(s)U(s) là điểm ở lá terminal, định nghĩa đệ quy là V(s)=max⁡V(s′)V(s)=\max V(s') ở lượt MAX và V(s)=min⁡V(s′)V(s)=\min V(s') ở lượt MIN. Đừng chọn lá lớn nhất của toàn cây: đường tới lá đó có thể đòi đối thủ hợp tác.

Python
def minimax(tree, maximizing=True):
    if isinstance(tree, (int, float)):
        return tree
    child_values = [minimax(child, not maximizing) for child in tree]
    return max(child_values) if maximizing else min(child_values)

game_tree = [[3, 5], [2, 9]]
branch_values = [minimax(branch, maximizing=False) for branch in game_tree]
print("Giá trị hai nhánh:", branch_values)
print("Minimax:", minimax(game_tree), "chọn A:", branch_values[0] > branch_values[1])
assert minimax(game_tree) == 3

Hỏi nhanh

Gốc là MAX, có hai nhánh MIN. Nhánh A có lá 4 và 6, nhánh B có lá 2 và 9. Vì sao MAX không chọn B để nhắm tới 9?

Alpha-beta pruning bỏ những nhánh đã chắc chắn không thay đổi quyết định minimax. Trong cây trên, xét A trước thì MAX đã bảo đảm được 3. Sang B, lá đầu là 2 nên MIN ở B sẽ trả không quá 2, kém hơn 3; lá 9 không cần đọc. Giá trị minimax giữ nguyên, còn thứ tự xét nước đi quyết định cắt được bao nhiêu: xét nước tốt trước thì cắt nhiều hơn.

Với cây đầy đủ hệ số nhánh bb và độ sâu dd, minimax có thể phải xét O(bd)O(b^d) lá. Vì vậy game lớn thường dùng giới hạn độ sâu và evaluation function. Khi cắt trước terminal, giá trị lá chỉ là ước lượng, nên không còn bảo đảm kết quả của cây trò chơi đầy đủ.

Hỏi nhanh

Gốc là MAX, có ba nhánh MIN, xét từ trái sang phải. A có lá 6 và 4, B có lá 3 và 8, C có lá 5 và 1. Alpha-beta không cần đọc lá nào?

Alpha-beta bỏ được lá 8 mà giá trị gốc vẫn là 4.64385144A≤ 3B1Csau A, MAX đã có 4; lá 3 ở B đủ biết B không hơn 4, nên bỏ lá 8
Alpha-beta bỏ được lá 8 mà giá trị gốc vẫn là 4.

Nếu có xúc xắc, tầng chance lấy kỳ vọng theo xác suất chứ không lấy min. Nếu đối thủ không phải người chơi đối kháng tối ưu, giả thiết minimax cũng cần cân nhắc. Ta phải xác định mô hình tương tác trước khi chọn phép gộp giá trị.

Evaluation function ở độ sâu cắt có thể là một model học từ ván đấu. Với game có hệ số nhánh rất lớn như cờ vây, Monte Carlo tree search (MCTS) không mở đều mọi nhánh mà dồn ngân sách mô phỏng vào nhánh hứa hẹn. Mỗi lượt mô phỏng đi từ gốc xuống; tại mỗi nút, chọn con ii có

vˉi+cln⁡Nni\bar v_i+c\sqrt{\frac{\ln N}{n_i}}

lớn nhất, với vˉi\bar v_i là điểm trung bình của con ii theo góc nhìn người chọn nước ở nút cha, nin_i là số lần thăm con đó, NN là số lần thăm nút cha; con chưa thăm được thử trước.

Tới nút chưa mở thì mở rộng, đánh giá bằng một ván chơi ngẫu nhiên tới cuối hoặc bằng một model, rồi cộng kết quả ngược lên mọi nút trên đường đã đi. Số hạng thứ nhất khai thác nhánh đang tốt, số hạng thứ hai ưu tiên nhánh còn ít được thử. Hết ngân sách thì chọn nước ở gốc được thăm nhiều nhất.

AlphaZero thay ván chơi ngẫu nhiên bằng value network và dùng policy network để ưu tiên nước đi.

Ràng buộc: không phải mọi bài đều hỏi đường đi

Nhà trường cần xếp ba nhóm A, B, C vào hai phòng 0 và 1. A và B phải khác phòng, B và C phải khác phòng; A được ấn định ở phòng 0. Ta không quan tâm thứ tự thao tác xếp, chỉ quan tâm bộ gán cuối có hợp lệ không.

Đây là bài toán thỏa mãn ràng buộc (constraint satisfaction problem, CSP): biến là phòng của mỗi nhóm, domain là {0,1}\{0,1\}, constraints là A≠BA\ne B, B≠CB\ne C, A=0A=0. Tính tay: A=0A=0 buộc B=1B=1; từ đó C=0C=0. A và C dùng chung phòng được vì đề không cấm. Tự thêm A≠CA\ne C sẽ làm một đề có nghiệm thành vô nghiệm.

A ở phòng 0 buộc B ở phòng 1 và C ở phòng 0.≠≠không có ràng buộc A-CAphòng 0Bphòng 1Cphòng 0A = 0
A ở phòng 0 buộc B ở phòng 1 và C ở phòng 0.

Quay lui gán từng biến và bỏ ngay nhánh đã vi phạm một điều kiện có đủ biến. Đây là pruning bằng tính hợp lệ; khác heuristic ước lượng chi phí của A*. Ta cài trên đúng ba biến rồi so với vét cạn toàn bộ 23=82^3=8 bộ gán.

Python
from itertools import product

variables = ["A", "B", "C"]
edges = [("A", "B"), ("B", "C")]

def consistent(assignment):
    if "A" in assignment and assignment["A"] != 0:
        return False
    for left, right in edges:
        if left in assignment and right in assignment:
            if assignment[left] == assignment[right]:
                return False
    return True

def backtrack(assignment):
    if len(assignment) == len(variables):
        return [assignment.copy()]
    variable = variables[len(assignment)]
    answers = []
    for room in [0, 1]:
        assignment[variable] = room
        if consistent(assignment):
            answers.extend(backtrack(assignment))
        del assignment[variable]
    return answers

csp_answers = backtrack({})
oracle_answers = []
for rooms in product([0, 1], repeat=3):
    A, B, C = rooms
    if A == 0 and A != B and B != C:
        oracle_answers.append(dict(zip(variables, rooms)))
assert csp_answers == oracle_answers
print("Nghiệm CSP:", csp_answers)
print("Khớp vét cạn:", csp_answers == oracle_answers)

Với nhiều biến, minimum remaining values (MRV) chọn biến còn ít giá trị hợp lệ nhất để sớm gặp nhánh thất bại. Forward checking loại khỏi domain các giá trị đã xung đột với biến mới gán. Những kỹ thuật này giảm công tìm, nhưng domain và constraints vẫn phải mô tả đúng bài; chúng không tạo thêm một điều kiện mà đề không có.

Hỏi nhanh

Thêm nhóm D phải khác phòng với cả A và B, vẫn chỉ có hai phòng, và A=0A = 0. Quay lui gán theo thứ tự A, B, C, D, có forward checking. Vô nghiệm được phát hiện sớm nhất vào lúc nào?

Forward checking phát hiện D hết giá trị nên bỏ nhánh ngay.ban đầuA0B0 1C0 1D0 1gán A = 0A0B1C0 1D1gán B = 1A0B1C0D∅D khác A và khác B: hai phòng đã bị chiếm, domain của D rỗng trước khi gán C
Forward checking phát hiện D hết giá trị nên bỏ nhánh ngay.

Forward checking chỉ nhìn từ biến vừa gán sang các láng giềng. Arc consistency xét mọi cặp: cung X→YX \to Y nhất quán khi với mỗi giá trị còn lại của XX, domain của YY còn ít nhất một giá trị đi kèm thỏa ràng buộc. Thuật toán AC-3 làm như sau:

  1. Đưa mọi cung Xi→XjX_i \to X_j của mọi ràng buộc vào hàng đợi.
  2. Lấy một cung ra, chạy REVISE: xóa khỏi domain của XiX_i các giá trị không có giá trị đi kèm trong domain của XjX_j.
  3. Nếu domain của XiX_i vừa bị xóa bớt, đưa lại vào hàng đợi các cung Xk→XiX_k \to X_i từ những láng giềng khác của XiX_i.
  4. Lặp tới khi hàng đợi rỗng, hoặc có domain rỗng, tức bài vô nghiệm.

AC-3 thường chạy trước hoặc xen giữa các bước quay lui để thu nhỏ domain. Nó chỉ kiểm từng cặp biến, nên domain còn nguyên chưa chứng tỏ bài có nghiệm.

Python
def ac3(domains, arcs):
    domains = {v: set(d) for v, d in domains.items()}
    queue = deque(arcs)
    while queue:
        x, y = queue.popleft()
        # mọi ràng buộc ở đây là ≠
        removed = {a for a in domains[x] if not any(a != b for b in domains[y])}
        if removed:
            domains[x] -= removed
            if not domains[x]:
                return None                      # domain rỗng: vô nghiệm
            queue.extend((z, x) for z, w in arcs if w == x and z != y)
    return domains

def both_ways(pairs):
    return [(a, b) for a, b in pairs] + [(b, a) for a, b in pairs]

rooms = {"A": {0}, "B": {0, 1}, "C": {0, 1}}
print("Chuỗi A ≠ B ≠ C, A = 0:", ac3(rooms, both_ways([("A", "B"), ("B", "C")])))
triangle = {"X": {0, 1}, "Y": {0, 1}, "Z": {0, 1}}
triangle_arcs = both_ways([("X", "Y"), ("Y", "Z"), ("Z", "X")])
print("Tam giác X, Y, Z đôi một khác:", ac3(triangle, triangle_arcs))

Ở bài xếp phòng, AC-3 tự suy ra đúng nghiệm duy nhất mà chưa cần quay lui. Ở tam giác, mỗi cặp riêng lẻ đều thỏa được, nên AC-3 không xóa gì. Nhưng ba biến đôi một khác nhau mà chỉ có hai giá trị thì vô nghiệm. Phải quay lui mới phát hiện được điều đó.

Tam giác ba biến đôi một khác, mỗi biến chỉ có hai giá trị: mỗi cung đều nhất quán mà bài vẫn vô nghiệm.≠≠≠X{0, 1}Y{0, 1}Z{0, 1}mỗi cung nhất quán:AC-3 không xóa gìnhưng 3 biến, 2 giá trị:vô nghiệm
Tam giác ba biến đôi một khác, mỗi biến chỉ có hai giá trị: mỗi cung đều nhất quán mà bài vẫn vô nghiệm.

Hỏi nhanh

Hai biến P,QP, Q có domain {1,2,3}\{1, 2, 3\} với ràng buộc P<QP < Q. Sau AC-3, domain của PP và QQ là gì?

Từ một nghiệm tới một kết luận logic

Tìm được một bộ gán hợp lệ trả lời câu hỏi “có thể xếp thế nào?”. Muốn kết luận “C bắt buộc ở phòng 0”, cần kiểm mọi bộ gán hợp lệ.

Đặt PP: A ở phòng 0; QQ: B ở phòng 0; RR: C ở phòng 0. Vì mỗi nhóm có đúng hai lựa chọn, điều kiện khác phòng tương đương XOR: một mệnh đề đúng, mệnh đề còn lại sai. Knowledge base (KB) là

P∧(P⊕Q)∧(Q⊕R).P\land(P\oplus Q)\land(Q\oplus R).

∧\land nghĩa là “và”, ¬\neg là “không”, ⊕\oplus là XOR. Ta cũng dùng ∨\lor (“hoặc”) và P⇒QP\Rightarrow Q (“nếu P thì Q”); mệnh đề kéo theo chỉ sai khi P đúng mà Q sai, nên tương đương ¬P∨Q\neg P\lor Q. Ta viết KB⊨RKB\models R (KB suy ra R) nếu mọi phép gán làm KB đúng cũng làm RR đúng; mỗi phép gán như vậy gọi là một model của KB. “Đúng trong một nghiệm” yếu hơn “đúng trong mọi nghiệm”.

Khi bỏ điều kiện PP, vẫn có cách xếp hợp lệ với C ở phòng 1, nên kết luận trước không còn được suy ra.

Python
worlds = list(product([False, True], repeat=3))
models = [(P, Q, R) for P, Q, R in worlds if P and (P != Q) and (Q != R)]
models_without_P = [(P, Q, R) for P, Q, R in worlds if (P != Q) and (Q != R)]
assert models                  # kiểm KB có nghiệm trước khi diễn giải
entails_R = all(R for P, Q, R in models)
counterexamples = [world for world in models_without_P if not world[2]]
print("Models của KB:", models)
print("KB suy ra R:", entails_R)
print("Bỏ P, phản ví dụ cho R:", counterexamples)
assert entails_R and counterexamples

all trên danh sách rỗng của Python trả True. Trong logic cổ điển, KB mâu thuẫn cũng suy ra mọi mệnh đề theo định nghĩa entailment, vì không còn model phản ví dụ. Với hệ thống xếp phòng, điều ta cần báo trước là không có cách xếp hợp lệ, không diễn giải đó thành bằng chứng C ở phòng 0. Vì vậy kiểm satisfiability trước khi đọc kết luận.

KB chỉ có một model và R đúng ở đó, nên KB suy ra R.PQRKBKB bỏ P0000000100010010110010000101111100011100model duy nhất: R đúngphản ví dụ: R sai
KB chỉ có một model và R đúng ở đó, nên KB suy ra R.

Hỏi nhanh

Quay lui tìm được một cách xếp hợp lệ có C ở phòng 0. Đã chứng minh được C luôn ở phòng 0 chưa?

Hỏi nhanh

Luật nói: nếu trời mưa thì sân ướt, tức M⇒UM \Rightarrow U. Quan sát thấy sân ướt. Với KB=(M⇒U)∧UKB = (M \Rightarrow U)\land U, có KB⊨MKB \models M không?

CNF và resolution: chứng minh mà không liệt kê hết

Bảng chân trị với nn mệnh đề có 2n2^n dòng. Resolution chứng minh bằng phản chứng, không cần liệt kê:

  1. Thêm ¬α\neg\alpha, phủ định điều cần chứng minh, vào KB.
  2. Đưa KB∧¬αKB \land \neg\alpha về dạng chuẩn tắc hội (CNF): một phép “và” của các clause, mỗi clause là phép “hoặc” của các literal (PP hoặc ¬P\neg P).
  3. Lặp luật resolution: từ hai clause chứa một cặp literal đối nhau, như P∨AP \lor A và ¬P∨B\neg P \lor B, sinh clause A∨BA \lor B.
  4. Sinh được clause rỗng là có mâu thuẫn, vậy KB⊨αKB \models \alpha. Không sinh thêm được clause mới nào thì KB⊭αKB \not\models \alpha.

Đưa về CNF có ba bước: thay P⇒QP \Rightarrow Q bằng ¬P∨Q\neg P \lor Q; đẩy ¬\neg vào trong bằng De Morgan, ¬(A∨B)=¬A∧¬B\neg(A \lor B) = \neg A \land \neg B; rồi phân phối ∨\lor vào ∧\land. Chẳng hạn (A∨B)⇒C(A \lor B) \Rightarrow C thành ¬(A∨B)∨C=(¬A∧¬B)∨C=(¬A∨C)∧(¬B∨C)\neg(A \lor B) \lor C = (\neg A \land \neg B) \lor C = (\neg A \lor C) \land (\neg B \lor C).

Thử với KB=(P∨Q)∧(¬P∨R)KB = (P \lor Q) \land (\neg P \lor R) và α=Q∨R\alpha = Q \lor R. Phủ định cho ¬Q∧¬R\neg Q \land \neg R, nên tập clause là {P,Q}\{P, Q\}, {¬P,R}\{\neg P, R\}, {¬Q}\{\neg Q\}, {¬R}\{\neg R\}. Mỗi clause là một tập literal; -x là phủ định của x.

Python
def resolve(c1, c2):
    out = []
    for lit in c1:
        if -lit in c2:
            out.append(frozenset((c1 - {lit}) | (c2 - {-lit})))
    return out

def refute(clauses):
    clauses = {frozenset(c) for c in clauses}
    while True:
        new = set()
        for c1 in clauses:
            for c2 in clauses:
                for r in resolve(c1, c2):
                    if not r:
                        return True              # clause rỗng: mâu thuẫn
                    new.add(r)
        if new <= clauses:
            return False                         # không sinh thêm được gì
        clauses |= new

P_, Q_, R_ = 1, 2, 3
kb = [{P_, Q_}, {-P_, R_}]
print("KB ⊨ Q ∨ R:", refute(kb + [{-Q_}, {-R_}]))
print("KB ⊨ Q:", refute(kb + [{-Q_}]))

Đường tới mâu thuẫn ở dòng đầu: {P,Q}\{P, Q\} với {¬Q}\{\neg Q\} cho {P}\{P\}; {P}\{P\} với {¬P,R}\{\neg P, R\} cho {R}\{R\}; {R}\{R\} với {¬R}\{\neg R\} cho clause rỗng. Dòng thứ hai không bao giờ ra clause rỗng, khớp với câu kiểm tra cuối bài: KB suy ra Q∨RQ \lor R nhưng không suy ra QQ.

Ba bước resolution đi từ các clause ban đầu tới clause rỗng.P ∨ Q¬Q¬P ∨ R¬RPR∅clause rỗng: mâu thuẫn
Ba bước resolution đi từ các clause ban đầu tới clause rỗng.

Logic mệnh đề chỉ có các câu đúng hoặc sai. Logic vị từ (first-order logic) thêm đối tượng, quan hệ, biến và lượng từ: ∀x (Meˋo(x)⇒CoˊVuˊ(x))\forall x\,(Mèo(x) \Rightarrow CóVú(x)) nghĩa là mọi con mèo đều là thú có vú, ∃x Meˋo(x)\exists x\,Mèo(x) nghĩa là có ít nhất một con mèo. Từ ∀x (Meˋo(x)⇒CoˊVuˊ(x))\forall x\,(Mèo(x) \Rightarrow CóVú(x)) và Meˋo(Tom)Mèo(Tom), muốn áp luật kéo theo thì phải tìm một phép thế cho biến làm hai literal trùng nhau: {x/Tom}\{x/Tom\} biến Meˋo(x)Mèo(x) thành Meˋo(Tom)Mèo(Tom). Việc tìm phép thế này gọi là unification (hợp nhất). Có phép thế rồi, Modus Ponens (từ AA và A⇒BA \Rightarrow B suy ra BB) cho CoˊVuˊ(Tom)CóVú(Tom). Resolution trên logic vị từ cũng dùng unification theo cách đó.

Hỏi nhanh

Câu (A∧B)⇒C(A \land B) \Rightarrow C viết thành CNF là gì?

Khi đi thi

  • Viết rõ trạng thái gồm những gì và đơn vị chi phí trước khi code.
  • Đề chấm đúng tối ưu thì dùng A với heuristic admissible; chấm theo chất lượng thì weighted A hay beam search cũng được.
  • Luôn giữ sẵn một lời giải hợp lệ, rồi cải thiện trong thời gian còn lại.

Tóm tắt

  • Tìm kiếm cần trạng thái đủ thông tin và đúng đại lượng chi phí. BFS, Dijkstra, A* khác nhau ở thứ tự mở rộng.
  • BFS giữ cả tầng biên, cỡ O(bd)O(b^d) nút; DFS chỉ giữ đường đang đi, cỡ O(bm)O(bm), nhưng không tối ưu; IDS có bộ nhớ của DFS mà vẫn gặp đích nông nhất.
  • Tìm kiếm cục bộ sửa dần một trạng thái; hill climbing kẹt ở cực đại địa phương, random restart và simulated annealing giúp thoát ra.
  • A* dùng g+hg+h; lower bound và cách xử lý đường tốt hơn giữ bảo đảm tối ưu. Heuristic quá lớn có thể cho đáp án sai.
  • Heuristic từ model hay weighted A* đổi bảo đảm tối ưu lấy tốc độ: nếu h≤w h∗h\leq w\,h^* thì chi phí không quá w C∗w\,C^*. Khi thời gian có hạn, giữ sẵn một lời giải hợp lệ rồi cải thiện dần.
  • Minimax xét phản ứng của đối thủ; giá trị của một nhánh không phải lá lớn nhất trong nhánh đó. Alpha-beta bỏ nhánh mà giữ giá trị; MCTS dồn ngân sách mô phỏng vào nhánh hứa hẹn.
  • CSP tìm phép gán thỏa mọi ràng buộc. AC-3 xóa các giá trị không có giá trị đi kèm ở láng giềng, nhưng chỉ kiểm từng cặp biến. Suy diễn (KB⊨RKB\models R) kiểm kết luận trên mọi model của KB; cần nhận ra KB vô nghiệm trước khi diễn giải.
  • Resolution chứng minh KB⊨αKB\models\alpha bằng cách đưa KB∧¬αKB\land\neg\alpha về CNF rồi tìm clause rỗng. Logic vị từ cần unification để khớp biến trước khi áp luật.

Kiểm tra cuối bài

Câu 1

Robot quay được bốn hướng, và mỗi hành động là quay trái, quay phải, hay tiến một ô, đều tốn 1. Robot đang đứng quay lên phía bắc, còn đích nằm thẳng về phía đông, cách 6 ô, trên một hàng không có tường. Heuristic hh là số ô còn lại tới đích, tức 6, bỏ qua việc quay. Chọn nhận định đúng về hh, và về chi phí tối ưu của kế hoạch bắt đầu bằng quay trái.

Phỏng theoCMU 10-601 · Exam 3 Practice · Q3.4

Câu 2

Đồ thị có cạnh âm: S→AS \to A giá 2, S→BS \to B giá 3, B→AB \to A giá −2-2, A→GA \to G giá 1. Dijkstra đóng đỉnh ngay khi lấy nó ra khỏi heap sẽ trả về chi phí S→GS \to G bằng bao nhiêu, và chi phí nhỏ nhất thật là bao nhiêu?

Câu 3

Heuristic hh admissible ở mọi đỉnh, nhưng có một cạnh u→vu \to v giá 1 với h(u)=6h(u) = 6 và h(v)=3h(v) = 3. Một bản A* đóng vĩnh viễn mỗi đỉnh ngay khi lấy nó ra khỏi heap. Nhận định nào đúng?

Câu 4

Lưới 10×1010\times 10, đi bốn hướng, mỗi bước giá 1, có thêm một cổng một chiều: đứng ở ô P=(0,9)P = (0, 9) có thể dịch tới Q=(9,0)Q = (9, 0) với giá 1. Đích là G=(9,1)G = (9, 1), và M(a,b)M(a, b) là khoảng cách Manhattan. Heuristic nào vẫn là lower bound ở mọi ô nn?

Câu 5

Từ trạng thái B có hai hành động. Sang A thì nhận thưởng 50, và giá trị của A hội tụ về 40. Sang C thì nhận thưởng 30, và giá trị của C hội tụ về 100. Với γ=0.9\gamma = 0.9, value iteration bắt đầu từ V=0V = 0 ở mọi trạng thái. Hành động tốt nhất ở B sau lần cập nhật đầu tiên, và sau khi các giá trị đã hội tụ, lần lượt là gì?

Phỏng theoCMU 10-601 · Exam 3 Practice · Q4.3

Câu 6

Đề game: sau nước đi của ta, bot đối thủ chọn ngẫu nhiên đều giữa hai nước, và điểm là trung bình qua 1000 ván. Nhánh A dẫn tới lá 5 hoặc 7, nhánh B dẫn tới lá 2 hoặc 12, đều là điểm của ta. Nên chọn nhánh nào?

Câu 7

MCTS ở gốc đã chạy 20 lượt mô phỏng. Nước A được thăm 10 lần, điểm trung bình 0.6. Nước B được thăm 2 lần, trung bình 0.5. Nước C được thăm 8 lần, trung bình 0.3. Con được chọn theo vˉi+ln⁡N/ni\bar v_i + \sqrt{\ln N/n_i}, với c=1c = 1. Lượt mô phỏng tiếp theo đi vào nhánh nào, và nếu hết ngân sách ngay lúc này thì nộp nước nào?

Câu 8

Xếp bốn nhóm A, B, C, D vào ba phòng, sao cho các cặp kề nhau trên vòng A-B-C-D-A khác phòng: A≠BA \ne B, B≠CB \ne C, C≠DC \ne D, D≠AD \ne A. Có bao nhiêu cách xếp?

Câu 9

KB=(P∨Q)∧(¬P∨R)KB = (P\lor Q)\land(\neg P\lor R). Kết luận nào được suy ra từ KB?

Câu 10

Đề tìm đường giới hạn mỗi test 2 giây. Điểm bằng độ dài tối ưu chia cho độ dài đường nộp, và không nộp được đường hợp lệ thì 0 điểm. Thử trên máy, A* với Manhattan xong trong 2 giây ở 70% số test, còn lại thì hết giờ. Chiến lược nào hợp lý nhất?

Đọc thêm

  • Russell, Norvig - Artificial Intelligence: A Modern Approach, tài liệu của tác giả. Đọc các phần search, adversarial search, CSP và logic; số chương thay đổi theo edition.
  • AIMA - Informed search. Đọc phần g+hg+h, admissibility, consistency và tạo heuristic từ bài toán được nới lỏng.
  • AIMA - Game playing. Dùng để đọc minimax, alpha-beta và giới hạn của evaluation ở độ sâu cắt.
  • Silver và cộng sự (2018) - A general reinforcement learning algorithm that masters chess, shogi, and Go through self-play, Science. AlphaZero: MCTS dùng policy network để ưu tiên nước đi và value network thay ván chơi ngẫu nhiên.
  • AIMA - Constraint satisfaction và Logical agents. Đọc domain, forward checking và định nghĩa entailment để phân biệt nghiệm với kết luận bắt buộc.