สร้างโจทย์ตัวอักษรแทนตัวเลขโดด

สัปดาห์ที่แล้ว เราหัดเขียนโปรแกรมทดลองแทนค่าตัวอักษรเพื่อแก้ปัญหาโจทย์ตัวอักษรกัน การบ้านระหว่างสัปดาห์ให้เด็กๆไปคิดโจทย์ประเภทนี้กัน และคิดว่าจะให้คอมพิวเตอร์ช่วยสร้างโจทย์ให้เราได้ไหม วันนี้เรามาดูตัวอย่างการสร้างโจทย์กัน

สมมุติว่าเราจะป้อนตัวเลขตัวตั้งเข้าไปสองตัว แล้วให้โปรแกรมสร้างโจทย์ที่บวกตัวตั้งทั้งสองให้ หน้าตาฟังก์ชั่นที่ใช้ทั้งหลายก็อาจจะเป็นแบบนี้ เรามีทั้ง to_digits/to_alphabets และ to_digits_loop/to_alphabets_loop เพื่อให้เห็นว่า list comprehension ใช้แทน for loop ได้อย่างไร สามารถเลือกใช้แบบ list comprehension หรือ loop ก็ได้:

In [1]:
def to_digits(x):
    """
    แปลงจำนวนเต็มบวกเป็นลิสต์ตัวเลขโดดแต่ละหลัก
    เช่น to_digits(3141) -> [3, 1, 4, 1]
    ฟังก์ชั่นนี้ใช้ list comprehension แทน loop
    ดูแบบ loop ใน to_digits_loop(x)
    to_digits และ to_digits_loop ให้คำตอบเหมือนกัน
    """
    digits = str(x)
    return [int(k) for k in digits]

def to_alphabets(x):
    """
    แปลงตัวเลขให้เป็นสตริงที่เลขโดด 0-9 ถูกแทนที่ด้วยอักษร A ถีง J
    เช่น to_alphabets(3141592653) -> 'DBEBFJCGFD'
    ฟังก์ชั่นนี้ใช้ list comprehension แทน loop
    ดูแบบ loop ใน to_alphabets_loop(x)
    """
    letters = "ABCDEFGHIJ"
    digits = to_digits(x)
    return "".join([letters[k] for k in digits])

def to_digits_loop(x):
    """
    แปลงจำนวนเต็มบวกเป็นลิสต์ตัวเลขโดดแต่ละหลัก
    เช่น to_digits_loop(3141) -> [3, 1, 4, 1]
    ฟังก์ชั่นนี้ใช้ loop แทน list comprehension
    ดูแบบ list comprehension ใน to_digits(x)
    to_digits และ to_digits_loop ให้คำตอบเหมือนกัน
    """
    digits = str(x)
    result = []
    for d in digits:
        result.append(int(d))
    return result

def to_alphabets_loop(x):
    """
    แปลงตัวเลขให้เป็นสตริงที่เลขโดด 0-9 ถูกแทนที่ด้วยอักษร A ถีง J
    เช่น to_alphabets(3141592653) -> 'DBEBFJCGFD'
    ฟังก์ชั่นนี้ใช้ loop แทน list comprehension 
    ดูแบบ list comprehension ใน to_alphabets(x)
    to_alphabets และ to_alphabets_loop ให้คำตอบเหมือนกัน
    """
    letters = "ABCDEFGHIJ"
    digits = to_digits(x)
    result = []
    for d in digits:
        result.append(letters[d])
    result = "".join(result)
    return result
    
def create_plus_problem(x, y):
    """
    สร้างโจทย์ตัวอักษรแทน x + y = z
    """
    z = x + y
    
    # แปลง x, y, z เป็นลิสต์ของเลขโดดอะไร
    xdigits = to_digits(x)
    ydigits = to_digits(y)
    zdigits = to_digits(z)
    
    # แปลง x, y เป็นตัวอักษร
    x_a = to_alphabets(x)
    y_a = to_alphabets(y)
    
    # แปลง z เป็นตัวอักษร ถ้ามีตัวอักษรที่ไม่เคยปรากฎใน x, y
    # ให้แสดงเป็นตัวเลขไว้
    z_a = []
    symbols = 'ABCDEFGHIJ'
    for d in zdigits:
        if (d not in xdigits) and (d not in ydigits):
            z_a.append(str(d))
        else:
            z_a.append(symbols[d])
            
    # เปลี่ยนลิสต์ของตัวอักษรเป็นข้อความด้วย "".join(list)        
    x_a = "".join(x_a)
    y_a = "".join(y_a)
    z_a = "".join(z_a)
    
    # พิมพ์โจทย์แบบทั้งตัวเลขและตัวอักษร
    result = f"{x} + {y} = {z}: {x_a} + {y_a} = {z_a}"
    return(result)
    

ทดลองใช้ create_plus_problem(...)

In [2]:
create_plus_problem(22,33)
Out[2]:
'22 + 33 = 55: CC + DD = 55'
In [3]:
create_plus_problem(123,312)
Out[3]:
'123 + 312 = 435: BCD + DBC = 4D5'
In [4]:
create_plus_problem(373,363)
Out[4]:
'373 + 363 = 736: DHD + DGD = HDG'
In [5]:
create_plus_problem(909,991)
Out[5]:
'909 + 991 = 1900: JAJ + JJB = BJAA'

เราจะเห็นได้ว่าโจทย์ที่ออกมาจาก create_plus_problem เป็นโจทย์ที่เดาได้ง่ายเพราะเลข 0-9 จะถูกเรียงลำดับแทนที่ด้วย A-J ทำให้ที่ไหนเราเห็น J เราก็รู้ว่าเป็นเลข 9 ที่ไหนเห็น A ก็รู้ว่าเป็นเลข 0 ฯลฯ ดังนั้นเราควรดัดแปลง create_plus_problem ให้สลับตัวอักษรให้ทายยากๆด้วย

ก่อนอื่นมาดูวิธีสลับตัวอักษรด้วย random.shuffle ก่อน (อ่านเรื่อง random.shuffle ได้ที่ Python random’s shuffle function to randomizes the sequence items หรือที่ ฟังก์ชันที่เกี่ยวข้องกับการสุ่มใน python)

In [6]:
import random #เราต้องการฟังก์ชั่นที่เกี่ยวกับการสุ่มมาใช้สลับตัวอักษร จึงต้องใช้ random

x = "ABCD" #สตริง x ที่เราจะสลับตัวอักษร

# เราจะใช้ random.shuffle(x) เลยไม่ได้เพราะสตริงในภาษาไพธอนไม่่สามารถเปลี่ยนแปลงโดยตรงๆได้
# ต้องเปลี่ยนเป็นลิสต์ แล้วสลับที่ แล้วค่อยเปลี่ยนกลับมาเป็นสตริงอีกที

y = list(x) #เปลี่ยนสตริง x เป็นลิสต์
random.shuffle(y) #สลับที่ของในลิสต์
y = "".join(y) #เปลี่ยนลิสต์จากบรรทัดบนกลับมาเป็นสตริง

print(x,y) # พิมพ์สตริงเริ่มต้น และสตริงที่สลับ
ABCD DBCA

เราจะแปลงอักษรจาก create_plus_problem โดยการสลับ 'ABCDEFGHIJ' ด้วย random.shuffle แล้วแทนค่า A-J ด้วยค่าในลำดับที่สลับแล้วนั้น

ไพธอนมีวิธีแทนค่าตัวอักษรในสตริงแบบตรงไปตรงมาโดยเราไม่ต้องทำเองคือวิธีที่เรียกว่า translate (อ่านเพิ่มเติมได้ที่ Python String translate()) เช่นถ้าเราจะแทนค่า a,b,c,k ด้ย A,B,C,K เราก็อาจทำอย่างนี้ได้:

In [7]:
x = "abcdefghijklmnop" #สตริงเริ่มต้น
trans = x.maketrans("abck","ABCK") #บอกว่าจะแทนอะไรด้วยอะไร
y = x.translate(trans) #ทำการแทนค่า
print(x) #พิมพ์ข้อความดั้งเดิม
print(y) #พิมพ์ข้อความที่แทนค่าตัวอักษร
abcdefghijklmnop
ABCdefghijKlmnop

ด้วย random.shuffle และ string translate เราก็มีเครื่องมือพร้อมแล้วสำหรับดัดแปลง create_plus_problem ให้สร้างปัญหาตัวอักษรที่เดายากขึ้น วิธีดัดแปลงก็อาจเป็นแบบนี้:

In [8]:
import random #เราต้องการฟังก์ชั่นที่เกี่ยวกับการสุ่มมาใช้สลับตัวอักษร จึงต้องใช้ random

def create_plus_problem_shuffle(x, y):
    """
    สร้างโจทย์ตัวอักษรแทน x + y = z
    มีการสลับตัวอักษรให้เดายากขึ้น
    """
    z = x + y
    
    # แปลง x, y, z เป็นลิสต์ของเลขโดดอะไร
    xdigits = to_digits(x)
    ydigits = to_digits(y)
    zdigits = to_digits(z)
    
    # แปลง x, y เป็นตัวอักษร
    x_a = to_alphabets(x)
    y_a = to_alphabets(y)
    
    #สร้างลำดับที่สลับไปมาของ A-J เก็บไว้ใน new_symbols
    symbols = 'ABCDEFGHIJ'
    new_symbols = list(symbols)
    random.shuffle(new_symbols)
    new_symbols = "".join(new_symbols)
    
    # แปลง z เป็นตัวอักษร ถ้ามีตัวอักษรที่ไม่เคยปรากฎใน x, y
    # ให้แสดงเป็นตัวเลขไว้
    z_a = []
    for d in zdigits:
        if (d not in xdigits) and (d not in ydigits):
            z_a.append(str(d))
        else:
            z_a.append(symbols[d])
            
    # เปลี่ยนลิสต์ของตัวอักษรเป็นข้อความด้วย "".join(list)        
    x_a = "".join(x_a)
    y_a = "".join(y_a)
    z_a = "".join(z_a)
    
    #ทำการสลับตัวอักษร A-J ให้เป็นลำดับใน new_symbols
    #เพื่อให้เดายาก
    trans = x_a.maketrans(symbols, new_symbols)
    x_a = x_a.translate(trans)
    y_a = y_a.translate(trans)
    z_a = z_a.translate(trans)
    
    # พิมพ์โจทย์แบบทั้งตัวเลขและตัวอักษร
    result = f"{x} + {y} = {z}: {x_a} + {y_a} = {z_a}"
    return(result)
    

เปรียบเทียบ create_plus_problem และ create_plus_problem_shuffle:

In [9]:
print(create_plus_problem(22,33))
print(create_plus_problem_shuffle(22,33))
22 + 33 = 55: CC + DD = 55
22 + 33 = 55: GG + CC = 55
In [10]:
print(create_plus_problem(123,312))
print(create_plus_problem_shuffle(123,312))
123 + 312 = 435: BCD + DBC = 4D5
123 + 312 = 435: AIG + GAI = 4G5
In [11]:
print(create_plus_problem(373,363))
print(create_plus_problem_shuffle(373,363))
373 + 363 = 736: DHD + DGD = HDG
373 + 363 = 736: IDI + IBI = DIB
In [12]:
print(create_plus_problem(909,991))
print(create_plus_problem_shuffle(909,991))
909 + 991 = 1900: JAJ + JJB = BJAA
909 + 991 = 1900: GFG + GGC = CGFF

ถ้าเราสนใจปัญหาอื่นๆที่ไม่ใช่บวก เราจะทำอย่างไรดี? เราอาจจะเขียนฟังก์ชั่นคล้ายๆ create_plus_problem_shuffle เพิ่มขึ้นมาสำหรับลบ คูณ และหาร แต่นั่นแปลว่าเราจะมีฟังก์ชั่นหลายๆฟังก์ชั่นที่ซ้ำซ้อนกัน ทำเกือบทุกอย่างเหมือนๆกัน ถ้ามีการแก้ไขอะไรเราก็ต้องตามไปแก้ในทุกๆฟังก์ชั่น วิธีที่ดีกว่าในกรณีนี้คือสร้างฟังก์ชั่นเดียวที่รับตัวแปรสามตัว x, y, op โดยที่ x, y เป็นตัวตั้งและ op บอกว่าจะให้บวกลบคูณหรือหารดีกว่า

In [13]:
import random #เราต้องการฟังก์ชั่นที่เกี่ยวกับการสุ่มมาใช้สลับตัวอักษร จึงต้องใช้ random


def create_problem_shuffle(x, y, op = '+'):
    """
    สร้างโจทย์ตัวอักษรแทน x op y = z
    op ควรเป็นหนึ่งใน '+', '-', '*', '/'
    ถ้าไม่ใส่ op เข้ามาจะกำหนดให้ op เป็น '+'
    ถ้า op เป็น '-' จะทำให้เฉพาะ y <= x
    ถ้า op เป็น '/' จะทำให้เฉพาะ x / y ไม่มีเศษเหลือ
    มีการสลับตัวอักษรให้เดายากขึ้น
    """
    
    #เลือกว่าจะบวกลบคูณหรือหาร
    if op not in ('+', '-', '*', '/'):
        return "รับเฉพาะ +, -, *, / เท่านั้น"
    
    if op == '+':
        z = x + y
    if op == '-':
        if x >= y:
            z = x - y
        else:
            return "ตัวตั้งต้องไม่น้อยกว่าตัวลบ"
    if op == '*':
        z = x * y
    if op == '/':
        if x % y == 0:
            z = x // y
        else:
            return "ทำเฉพาะกรณีหารลงตัวเท่านั้น"
        
    
    # แปลง x, y, z เป็นลิสต์ของเลขโดดอะไร
    xdigits = to_digits(x)
    ydigits = to_digits(y)
    zdigits = to_digits(z)
    
    # แปลง x, y เป็นตัวอักษร
    x_a = to_alphabets(x)
    y_a = to_alphabets(y)
    
    #สร้างลำดับที่สลับไปมาของ A-J เก็บไว้ใน new_symbols
    symbols = 'ABCDEFGHIJ'
    new_symbols = list(symbols)
    random.shuffle(new_symbols)
    new_symbols = "".join(new_symbols)
    
    # แปลง z เป็นตัวอักษร ถ้ามีตัวอักษรที่ไม่เคยปรากฎใน x, y
    # ให้แสดงเป็นตัวเลขไว้
    z_a = []
    for d in zdigits:
        if (d not in xdigits) and (d not in ydigits):
            z_a.append(str(d))
        else:
            z_a.append(symbols[d])
            
    # เปลี่ยนลิสต์ของตัวอักษรเป็นข้อความด้วย "".join(list)        
    x_a = "".join(x_a)
    y_a = "".join(y_a)
    z_a = "".join(z_a)
    
    #ทำการสลับตัวอักษร A-J ให้เป็นลำดับใน new_symbols
    #เพื่อให้เดายาก
    trans = x_a.maketrans(symbols, new_symbols)
    x_a = x_a.translate(trans)
    y_a = y_a.translate(trans)
    z_a = z_a.translate(trans)
    
    # พิมพ์โจทย์แบบทั้งตัวเลขและตัวอักษร
    result = f"{x}{op}{y} = {z}: {x_a}{op}{y_a} = {z_a}"
    return(result)

ทดลองใช้ create_problem_shuffle:

In [14]:
create_problem_shuffle(169,13,'*')
Out[14]:
'169*13 = 2197: GEA*GJ = 2GA7'
In [15]:
create_problem_shuffle(13,113,'/')
Out[15]:
'ทำเฉพาะกรณีหารลงตัวเท่านั้น'
In [16]:
create_problem_shuffle(156, 942, '*')
Out[16]:
'156*942 = 146952: EFH*BDC = EDHBFC'
In [17]:
#สร้างโจทย์สุ่มๆมาสัก 20 ข้อ

import random

for n in range(0,20):
    x = random.randint(0,10000) #สุ่ม x มาจาก 0 ถึง 10,000
    y = random.randint(0,10000) #สุ่ม y มาจาก 0 ถึง 10,000
    op = random.choice(('+', '-', '*', '/'))
    print(create_problem_shuffle(x,y,op))
        
1487*6349 = 9440963: AIGE*CJID = DII0DCJ
2324*1300 = 3021200: AGAI*JGDD = GDAJADD
8305+547 = 8852: AIFJ+JDC = AAJ2
9671*3506 = 33906526: DAIG*CEFA = CCDFAE2A
6251-2975 = 3276: DBIF-BJEI = 3BED
ทำเฉพาะกรณีหารลงตัวเท่านั้น
5622*4916 = 27637752: AEFF*CDIE = F7E377AF
6156+8678 = 14834: DHAD+JDEJ = H4J34
ทำเฉพาะกรณีหารลงตัวเท่านั้น
7524+554 = 8078: CJBH+JJH = 80C8
1068*8621 = 9207228: AJGI*IGHA = 9HJ7HHI
ตัวตั้งต้องไม่น้อยกว่าตัวลบ
ทำเฉพาะกรณีหารลงตัวเท่านั้น
ทำเฉพาะกรณีหารลงตัวเท่านั้น
5598-2139 = 3459: BBGJ-HCAG = A4BG
8531*4639 = 39575309: EHGI*CJGB = GBH7HG0B
ทำเฉพาะกรณีหารลงตัวเท่านั้น
7395+9298 = 16693: HBEJ+EDEC = 166EB
3508*8593 = 30144244: ECHI*ICJE = EH144244
2659+1129 = 3788: BJGA+HHBA = 3788

ฟังก์ชั่น create_problem_shuffle มีข้อจำกัดเกี่ยวกับโจทย์ลบและหาร ดังนั้นเราอาจต้องช่วยมันหน่อยโดยการเลือกค่า x, y ที่เหมาะสมใส่เข้าไป เช่น:

In [18]:
#สำหรับโจทย์ลบ เลือก y ให้ไม่เกิน x
for n in range(0,10):
    x = random.randint(0,10000)
    y = random.randint(0,x) 
    op = '-'
    print(create_problem_shuffle(x,y,op))
3287-3093 = 194: BAID-BJFB = 1F4
4327-865 = 3462: DJCG-AIH = JDIC
7266-7253 = 13: JDAA-JDFB = 1B
2854-2199 = 655: BCED-BGAA = 6EE
1187-86 = 1101: FFJG-JA = FF0F
4286-4283 = 3: FEHI-FEHB = B
4744-2417 = 2327: CJCC-FCAJ = F3FJ
7078-5108 = 1970: CHCB-AGHB = G9CH
8245-2372 = 5873: HADI-ACBA = IHBC
3212-1638 = 1574: EJCJ-CFEH = C574
In [19]:
#สำหรับโจทย์หาร เลือก y ให้หาร x ลงตัว
for n in range(0,10):
    x = random.randint(0,10000)
    while True:   #วนสุ่ม y ในช่วง 1 ถึง x//2 จนเจอ y ที่หาร x ลงตัว
        y = random.randint(1,x//2)
        if x % y == 0:
            break
    op = '/'
    print(create_problem_shuffle(x,y,op))
5317/409 = 13: ICHA/BDG = HC
8044/4022 = 2: AIBB/BIHH = H
4541/19 = 239: AHAG/GD = 23D
9846/547 = 18: HIGF/CGJ = 1I
2937/3 = 979: CIDF/D = IFI
9344/64 = 146: DFHH/GH = 1HG
7340/367 = 20: BIED/ICB = 2D
420/42 = 10: DHI/DH = 1I
1399/1 = 1399: FEGG/F = FEGG
1095/5 = 219: ICAJ/J = 2IA
In [ ]: