[0]zero-index clytie0x/zero-index

2.4 อ่านโจทย์แบบ grader

ในการสอบ TOI Zero และค่าย สอวน. ไม่มีคนนั่งอ่านโค้ดของเรา grader จะรันโค้ดกับข้อมูลชุดที่เราไม่เคยเห็น แล้วเทียบผลลัพธ์กับคำตอบ โค้ดที่คิดถูกแต่พิมพ์ผลเกินมาหนึ่งคำก็ได้ศูนย์ หน้านี้รวมทุกอย่างจากสามหน้าก่อน มาใช้อ่านโจทย์จริงหนึ่งข้อ เขียนทีละขั้นจนเสร็จ แล้วทดสอบแบบที่ grader ทดสอบ

grader ทำงานยังไง

  1. คอมไพล์โค้ดที่ส่ง ถ้าคอมไพล์ไม่ผ่าน ก็จบตรงนั้น
  2. รันโปรแกรมกับชุดทดสอบ (test case) ทีละชุด แต่ละชุดคือข้อมูลเข้าหนึ่งก้อน ป้อนให้โปรแกรมแบบเดียวกับที่เราพิมพ์ในหน้าต่างดำ
  3. เก็บทุกอย่างที่โปรแกรมพิมพ์ออกมา เอาไปเทียบกับคำตอบของชุดนั้นทีละตัวอักษร
  4. ให้ผลแต่ละชุด เช่น ผ่าน คำตอบผิด ใช้เวลาเกิน หรือโปรแกรมพังกลางทางอย่างหารด้วยศูนย์ในหน้า 1.2 คะแนนมักคิดตามจำนวนชุดที่ผ่าน

grader ไม่เห็นหน้าจอ ไม่เห็นบรรทัด Process returned ไม่อ่านคำอธิบาย และไม่เดาว่าเราตั้งใจพิมพ์อะไร มันเห็นแค่ตัวอักษรที่ printf พิมพ์ออกมาเท่านั้น

ส่วนประกอบของโจทย์

โจทย์ตัวอย่าง ยอดขายสหกรณ์

สหกรณ์โรงเรียนขายสมุดราคาเล่มละ P บาท วันหนึ่งขายได้ N เล่ม ให้พิมพ์รายงานยอดขายของวันนั้น

ข้อมูลเข้า บรรทัดแรกเป็นวันที่ขาย ในรูปแบบ D/M/Y เมื่อ Y เป็นปี พ.ศ. (1 ≤ D ≤ 31, 1 ≤ M ≤ 12, 2500 ≤ Y ≤ 2600) บรรทัดที่สองมีจำนวนเต็ม N และ P คั่นด้วยช่องว่าง (1 ≤ N, P ≤ 100000)

ข้อมูลออก บรรทัดแรกพิมพ์ Date ตามด้วยวันที่ในรูปแบบ DD/MM/YYYY ที่วันและเดือนเป็นเลขสองหลักเสมอ บรรทัดที่สองพิมพ์ Total ตามด้วยยอดขายและคำว่า baht ทุกส่วนคั่นด้วยช่องว่างหนึ่งช่อง

5/3/2569
120 25
Date 05/03/2569
Total 3000 baht

โจทย์แบบ grader เกือบทุกข้อมีห้าส่วนนี้ แต่ละส่วนตอบคำถามคนละข้อ

ส่วนใช้ตัดสินใจเรื่อง
เรื่องราวต้องคำนวณอะไร
ข้อมูลเข้าเขียน scanf ยังไง กี่ตัว ตัวคั่นคืออะไร
ขอบเขตใช้ int หรือ long long กล่อง %s กี่ช่อง ตารางกว้างเท่าไร
ข้อมูลออกเขียน printf ยังไง ทุกตัวอักษร
ตัวอย่างเช็กว่าเข้าใจโจทย์ถูก และเป็นชุดทดสอบชุดแรกของเรา

ขั้นที่ 1 อ่านข้อมูลเข้า

บรรทัดแรกเป็นวันที่คั่นด้วย / ใช้ "%d/%d/%d" แบบในหน้า 2.3 บรรทัดที่สองเป็นตัวเลขสองตัว เขียน scanf แยกตามบรรทัดจะอ่านง่าย ส่วนการขึ้นบรรทัดระหว่างสองบรรทัดนั้นไม่ต้องสนใจ %d ข้ามให้เอง

ขั้นที่ 2 ดูขอบเขต

ยอดขายคือ N × P ซึ่งมากสุดได้ 100000 × 100000 = 1010 เกิน 2 × 109 ของ int ตามตารางในหน้า 1.3 จึงต้องเก็บ N กับ P เป็น long long ตั้งแต่ตอนรับ ส่วนวัน เดือน ปี เล็กมาก ใช้ int ได้

ขั้นที่ 3 อ่านข้อมูลออกทีละตัวอักษร

เทียบกับผลลัพธ์ตัวอย่าง แล้วแปลงเป็นข้อความแม่แบบของ printf

  • Date ตัว D ใหญ่ ตามด้วยช่องว่างหนึ่งช่อง วันที่ต้องได้ 05/03 ทั้งที่ข้อมูลเข้าเป็น 5/3 จึงใช้ %02d จากหน้า 2.2 ส่วนปีมี 4 หลักอยู่แล้ว ใช้ %d
  • Total ตัว T ใหญ่ ช่องว่าง ตัวเลข ช่องว่าง แล้ว baht ตัวเล็กทั้งคำ
  • สองบรรทัด จึงมี \n ท้ายทั้งสองบรรทัด

ขั้นที่ 4 เขียนโปรแกรม

#include <stdio.h>

int main()
{
    int day, month, year;
    long long books, price;
    scanf("%d/%d/%d", &day, &month, &year);
    scanf("%lld %lld", &books, &price);
    printf("Date %02d/%02d/%d\n", day, month, year);
    printf("Total %lld baht\n", books * price);
    return 0;
}
5/3/2569
120 25
Date 05/03/2569
Total 3000 baht

ขั้นที่ 5 ทดสอบด้วยตัวอย่าง

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

  1. คัดลอกข้อมูลเข้าตัวอย่าง ในเว็บนี้เลื่อนเมาส์ไปวางบนกล่องข้อมูลเข้า จะมีปุ่มคัดลอกที่มุมขวาบน ถ้าเป็นโจทย์ไฟล์ PDF ให้ลากคลุมตัวอย่างแล้วกด Ctrl+C
  2. กด F9 รอจนหน้าต่างดำขึ้นและว่างอยู่
  3. คลิกขวาในหน้าต่างดำ หรือกด Ctrl+V ถ้ามีกล่องถามยืนยันว่าจะวางข้อความหลายบรรทัด ให้กดยืนยัน
  4. ถ้าโปรแกรมยังนิ่งอยู่ แปลว่าบรรทัดสุดท้ายที่คัดลอกมาไม่มีการขึ้นบรรทัดปิดท้าย กด Enter อีกครั้ง

หน้าต่างดำจะแสดงข้อมูลเข้าที่วางไว้ก่อน ตามด้วยผลลัพธ์ หน้าตาแบบนี้

5/3/2569
120 25
Date 05/03/2569
Total 3000 baht

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

ขั้นที่ 6 ทดสอบค่าขอบ

ผ่านตัวอย่างยังไม่พอ ชุดทดสอบของ grader มีค่าที่โจทย์อนุญาตทุกแบบ โดยเฉพาะค่ามากสุดกับน้อยสุด ให้แต่งข้อมูลเข้าเองจากขอบเขต แล้วคิดคำตอบด้วยมือเทียบ ถ้าขั้นที่ 2 พลาดไปใช้ int ค่ามากสุดจะจับได้ทันที

#include <stdio.h>

int main()
{
    int day, month, year;
    int books, price;
    scanf("%d/%d/%d", &day, &month, &year);
    scanf("%d %d", &books, &price);
    printf("Date %02d/%02d/%d\n", day, month, year);
    printf("Total %d baht\n", books * price);
    return 0;
}
31/12/2600
100000 100000
Date 31/12/2600
Total 1410065408 baht

คำตอบที่ถูกคือ 10000000000 ซึ่งโปรแกรมในขั้นที่ 4 พิมพ์ได้ถูก ส่วนแบบ int ได้เลขบวกที่ดูปกติ ไม่ติดลบเหมือนตัวอย่างในหน้า 1.3 ถ้าไม่ได้คิดคำตอบด้วยมือไว้ก่อน จะดูไม่ออกเลยว่าผิด

ชุดทดสอบที่ควรลองเองทุกข้อ

ตัวอย่างในโจทย์ ค่าน้อยสุดทุกตัว ค่ามากสุดทุกตัว และค่าที่ทำให้รูปแบบผลลัพธ์เปลี่ยน อย่างวันที่หลักเดียวกับสองหลักในข้อนี้

ผลลัพธ์ต้องตรงทุกตัวอักษร

ถ้าคำตอบที่ต้องได้คือ Total 3000 baht ผลเหล่านี้ไม่ผ่านทั้งหมด แม้ตัวเลขจะถูก

โปรแกรมพิมพ์ผิดตรงไหน
total 3000 bahtตัวพิมพ์เล็กพิมพ์ใหญ่ต่างกัน
Total 3000 Bahtเหมือนกัน
Total  3000 bahtช่องว่างสองช่อง
Total 3000bahtขาดช่องว่าง
Total 3000.00 bahtคำตอบเป็นจำนวนเต็ม แต่พิมพ์ทศนิยม
Total = 3000 bahtมีเครื่องหมายเกินมา

เรื่องที่ตาเปล่ามองไม่เห็น

  • ช่องว่างท้ายบรรทัด เช่น printf("Total %lld baht \n", ...) มีช่องว่างหลัง baht grader บางระบบตัดช่องว่างท้ายบรรทัดทิ้งก่อนเทียบ บางระบบไม่ตัด อย่าเสี่ยง ให้ไม่มีเลย
  • การขึ้นบรรทัด ทุกบรรทัดจบด้วย \n รวมถึงบรรทัดสุดท้าย และไม่พิมพ์บรรทัดว่างเกินมา
  • ข้อความที่ใส่ไว้ตรวจเอง เช่น [ ] จากหน้า 2.2 หรือการพิมพ์ค่าที่รับมาดู ต้องลบออกก่อนส่ง

ข้อมูลเข้าที่ไม่บอกจำนวน

โจทย์ในบทนี้บอกเสมอว่ามีข้อมูลกี่ตัว จึงเขียน scanf ได้ครบ แต่บางโจทย์ให้ "รับตัวเลขไปเรื่อยๆ จนจบข้อมูลเข้า" หรือให้จำนวน N มาก่อนแล้วตามด้วยตัวเลข N ตัว แบบนี้ต้องใช้วนซ้ำ ซึ่งจะเรียนในบทที่ 4

ลองทำให้พัง

แบบที่ 1 พิมพ์ข้อความบอกให้กรอก

โปรแกรมที่เขียนให้คนใช้ มักบอกก่อนว่าต้องพิมพ์อะไร ลองเพิ่มเข้าไปในโปรแกรม coop

#include <stdio.h>

int main()
{
    int day, month, year;
    long long books, price;
    printf("Enter date: ");
    scanf("%d/%d/%d", &day, &month, &year);
    printf("Enter books and price: ");
    scanf("%lld %lld", &books, &price);
    printf("Date %02d/%02d/%d\n", day, month, year);
    printf("Total %lld baht\n", books * price);
    return 0;
}
5/3/2569
120 25
Enter date: Enter books and price: Date 05/03/2569
Total 3000 baht

นี่คือสิ่งที่ grader เห็น บรรทัดแรกไม่ตรงกับ Date 05/03/2569 ข้อนี้ได้ผิดทุกชุด ทั้งที่คำนวณถูกหมด ข้อความบอกให้กรอกมีประโยชน์กับคนเท่านั้น grader ไม่ต้องการ พิมพ์เฉพาะสิ่งที่ข้อมูลออกสั่งเท่านั้น

แบบที่ 2 ลืม \n

ลบ \n ออกจาก printf บรรทัด Date

#include <stdio.h>

int main()
{
    int day, month, year;
    long long books, price;
    scanf("%d/%d/%d", &day, &month, &year);
    scanf("%lld %lld", &books, &price);
    printf("Date %02d/%02d/%d", day, month, year);
    printf("Total %lld baht\n", books * price);
    return 0;
}
5/3/2569
120 25
Date 05/03/2569Total 3000 baht

printf สองครั้งไม่ได้แปลว่าสองบรรทัด ข้อความจะต่อกันไปเรื่อยๆ จนกว่าจะเจอ \n ทั้งสองค่าถูก แต่รวมเป็นบรรทัดเดียว grader ให้ผิด

สรุป

  • grader เห็นแค่สิ่งที่ printf พิมพ์ และเทียบทีละตัวอักษร
  • ข้อมูลเข้าบอกวิธีเขียน scanf ขอบเขตบอกชนิดตัวแปร ข้อมูลออกบอกข้อความแม่แบบของ printf
  • ทดสอบด้วยการคัดลอกข้อมูลเข้าทั้งก้อนไปวางในหน้าต่างดำ
  • ทดสอบค่ามากสุดน้อยสุดเองเสมอ และคิดคำตอบด้วยมือไว้เทียบ
  • ห้ามพิมพ์ข้อความบอกให้กรอก ห้ามมีช่องว่างเกิน และทุกบรรทัดจบด้วย \n

กลับไปเช็กรายการ ผ่านบทนี้เมื่อ แล้วไปทำแบบฝึกหัด