Ngoài việc học sinh phải chú ý xây dựng thuật toán tối ưu để đảm bảo thời gian thực hiện chương trình theo quy định không quá 1 giây/1 test thì học sinh còn cần phải chú ý về kiểu dữ liệ
Trang 11 MỞ ĐẦU
1.1 Lý do chọn đề tài
Để bắt nhịp xu thế hiện đại, trong hướng dẫn điều chỉnh nội dung dạy học Tin học năm 2021- 2022 Bộ Giáo dục đã định hướng các trường THPT ngừng dạy học NNLT Pascal, thay thế bằng các NNLT tiên tiến hơn như C, C++, Python… Năm học 2021- 2022 là năm đầu tiên trường chúng tôi đưa NNLT C+ + vào các bài dạy học Tin học chính khoá cho học sinh khối 11 và ôn luyện thi học sinh giỏi
Đã từ lâu, công tác bồi dưỡng học sinh khá, giỏi được xem là nhiệm vụ trọng tâm của mỗi nhà trường Thông qua kết quả thi học sinh giỏi, phần nào giúp khẳng định vị thế của nhà trường đối với các trường trong huyện trong tỉnh Đối với môn Tin học, học lập trình góp phần phát triển tư duy sáng tạo, rèn luyện tính cẩn thận tính chính xác cho người học; đồng thời việc học lập trình ngay từ THPT góp phần phát hiện tài năng, định hướng nghề nghiệp cho học sinh trước khi lựa chọn ngành nghề, bổ sung nguồn nhân lực tin học cho một xã hội công nghệ thông tin
Các đề thi học sinh giỏi môn Tin học cấp THPT thường yêu cầu xử lí các
bộ dữ liệu có phạm vi khá lớn Ngoài việc học sinh phải chú ý xây dựng thuật toán tối ưu để đảm bảo thời gian thực hiện chương trình (theo quy định không quá 1 giây/1 test) thì học sinh còn cần phải chú ý về kiểu dữ liệu của các biến được sử dụng trong chương trình
Ép kiểu là một kĩ thuật được sử dụng phổ biến được sử dụng trong các bài thi học sinh giỏi
Trên thực tế đã có rất nhiều tài liệu có đề cập đến kĩ thuật ép kiểu nhưng các các tài liệu này chỉ đưa ra code của bài toán, chưa phân tích lí do cần ép kiểu, cách tư duy, cách cài đặt Do đó khi áp dụng vào các bài toán khác nhau học sinh thường lúng túng quên không thực hiện ép kiểu hoặc thực hiện ép kiểu nhưng chưa đúng dẫn đến kết quả đưa ra (output) bị sai
Từ lí do trên tôi xin trình bày sáng kiến kinh nghiệm “RÈN LUYỆN TƯ DUY LẬP TRÌNH CHO HỌC SINH KHÁ GIỎI THÔNG QUA VIỆC PHÂN TÍCH, ĐÁNH GIÁ NHIỀU CÁCH GIẢI KHÁC NHAU CỦA MỘT
SỐ BÀI TẬP TIN HỌC CÓ SỬ DỤNG KĨ THUẬT ÉP KIỂU TRONG NGÔN NGỮ LẬP TRÌNH C++”
1.2 Mục đích nghiên cứu
Đề tài này đưa ra một số bài tập cần sử dụng kĩ thuật ép kiểu, các cách cài đặt code mà học sinh thường thực hiện, phân tích đánh giá các cách giải Thông qua đó học sinh sẽ biết chú ý hơn đến việc xem xét kĩ dữ liệu, cân nhắc khi khai
Trang 2báo biến, cài đặt thuật toán, đồng thời qua đó giúp học sinh rèn luyện tư duy lập trình
1.3 Đối tượng nghiên cứu
Các bài tập lập trình có sử dụng ép kiểu trong chương trình Tin học THPT
1.4 Phương pháp nghiên cứu
Trong quá trình nghiên cứu và hoàn thiện SKKN này tôi đã sử dụng nhiều phương pháp:
- Phương pháp nghiên cứu tài liệu: Tôi đã nghiên cứu các bài tập lập trình
đề thi môn Tin học trong nhiều năm trở lại đây, nghiên cứu các bài làm của học sinh, tổng hợp các cách giải
- Phương pháp xử lí số liệu: Dựa vào kết quả chấm điểm từ phần mềm
chấm bài tự động Themis trên cùng một bộ Test chấm của các cách làm, phân tích các lỗi sai, từ đó rèn luyện tư duy lập trình cho học sinh
Trang 32 PHẦN NỘI DUNG
2.1 Cơ sở lí luận
Ép kiểu trong C, C++ là việc gán giá trị của một biến có kiểu dữ liệu này
tới biến khác có kiểu dữ liệu khác
Cú pháp: (type) value;
Ví dụ:
float c = 35.8f;
int b = (int)c + 1;
Trong ví dụ trên, đầu tiên giá trị dấu phẩy động c được đổi thành giá trị nguyên 35 Sau đó nó được cộng với 1 và kết quả là giá trị 36 được lưu vào b.1
2.2 Thực trạng vấn đề trước khi áp dụng sáng kiến.
Năm học 2021-2022, được sự thống nhất của Tổ nhóm chuyên môn, chúng tôi đã lựa chọn NNLT C++ để đưa vào minh hoạ cho các bài dạy chính khoá Tin học 11 và ôn thi học sinh giỏi (thay thế cho NNLT Pascal) Tuy nhiên, do chưa
có sách giáo khoa thay thế nên cô trò vẫn sử dụng sách giáo khoa Tin học hiện hành, việc tổ chức dạy học, chỉ dẫn tìm kiếm tài liệu cho học sinh tham khảo gặp không ít khó khăn
Trong quá trình ôn luyện học sinh giỏi tôi thường không máy móc yêu cầu học sinh phải thực hiện cùng một cách làm, mà học sinh có thể thực hiện giải theo nhiều cách khác nhau Để nâng cao hiệu quả dạy học, tôi sử dụng phần mềm chấm bài tự động Themis (tác giả Lê Minh Hoàng) để kiểm tra kết quả Sau khi chấm, thông báo kết quả tôi thường yêu cầu học sinh phải xem xét kĩ từng cách làm của các bạn trong nhóm đội tuyển, tìm ra cái hay cái dở trong từng code chương trình, để từ đó học sinh rèn luyện tư duy lập trình cho học sinh Trong quá trình thảo luận, trao đổi cách làm, tôi nhận thấy nhiều học sinh
tỏ ra khá lúng túng bởi các em không hiểu tại sao cùng một ý tưởng, cùng thuật toán của nhưng kết quả lại khác nhau Có bạn ghi điểm tuyệt đối, có bạn chỉ ghi được một phần nhỏ số điểm, lại cũng có bạn không ghi được điểm nào Nguyên nhân có thể rất nhiều, nhưng trong phạm vi sáng kiến kinh nghiệm này tôi chỉ đưa ra lỗi do học sinh không thực hiện ép kiểu trong các bài tập cần ép kiểu
1 Từ ép kiểu trong C++ … đến lưu vào b trích dẫn từ nguồn: https://viettuts.vn/lap-trinh-cpp/ep-kieu-trong-cpp
Trang 4hoặc có ép kiểu nhưng thực hiện chưa đúng dẫn tới kết quả chưa được như mong muốn thông qua việc phân tích đánh giá các cách làm
2.3 Các biện pháp đã sử dụng để giải quyết vấn đề
Cách rèn luyện tư duy lập trình tốt nhất đó là thực hành giải các bài tập Quá trình giải hệ thống các bài tập, học sinh buộc phải động não suy nghĩ để xây dựng thuật toán, viết code, khi đó học sinh mới thực sự tích luỹ và phát triển tư duy, biến các kiến thức từ sách vở thành các kiến thức của bản thân Để học sinh tránh các lỗi sai, với mỗi dạng bài giáo viên cần tạo tình huống có vấn
đề, để học sinh giải quyết vấn đề và từ đó tích luỹ kinh nghiệm cho bản thân Như vậy để học sinh hiểu rõ kĩ thuật ép kiểu, tránh lỗi sai khi ép kiểu tôi đã xây
dựng hệ thống các bài tập có sử dụng ép kiểu Để đưa học sinh vào tình huống
có vấn đề tôi thường lựa chọn các bài tập học sinh dễ sai nhất Quá trình học sinh giải bài tập, trao đổi với bạn cùng nhóm về cách giải của mình và của bạn chính là học sinh đang tự giải quyết vấn đề Thông qua đây học sinh sẽ tích luỹ được thêm kinh nghiệm cho bản thân, rèn luyện tư duy lập trình, nâng cao hiệu quả học tập
Dưới đây là một số bài tập ép kiểu được tôi sử dụng để đưa học sinh vào tình huống có vấn đề
2.3.1 Bài toán ví dụ
Bài toán 1: Tổng nhỏ nhất Tên file: MINSUM.CPP
Với hai số nguyên dương A, B cho trước Ta dễ dàng tìm được ước chung lớn nhất G và bội chung nhỏ nhất L của hai số A và B
Bây giờ chúng ta hãy xét bài toán ngược của bài toán trên:
“Cho biết trước ước chung lớn nhất G và bội chung nhỏ nhất L của hai số nguyên dương A và B.
Rõ ràng, sẽ có rất nhiều cặp (A, B) nguyên dương có ước chung lớn nhất là
G và bộ chung nhỏ nhất là L, tuy nhiên cũng có trường hợp chúng ta không thể tìm được giá trị A, B thỏa mãn Vì vậy, hãy xác định giá trị nhỏ nhất của tổng A + B, hoặc đưa ra -1 nếu không tìm được cặp (A, B)”.
INPUT: Hai số nguyên dương G và L (1 ≤ G ≤ L ≤ 109)
OUTPUT: Số nguyên dương là tổng nhỏ nhất có thể Trong trường hợp
không tìm được hai số A và B thì đưa ra kết quả là -1
Ví dụ:
Trang 5MINSUM.IN P
MINSUM.OU T
Giải thích ví dụ:
- Ở ví dụ thứ nhất: Chỉ có cặp (2, 10) thỏa mãn UCLN(2,10) = 2,
BCNN(2,10) = 10 Nên tổng là 12
- Ở ví dụ thứ hai: Có hai cặp (2, 20) và (4, 10) thỏa mãn, tổng nhỏ nhất có
thể là 14
- Ở ví dụ thứ ba: không tìm được cặp nào thỏa mãn UCLN là 3 và BCNN
là 5
Ràng buộc
40% số điểm tương ứng với số test có 1 ≤ G ≤ L ≤ 100;
60% số điểm còn lại không có ràng buộc gì
Ý tưởng thuật toán
Nhận xét: Để giải bài tập này ta có thể sử dụng cách trâu bò đó là dùng 2 vòng lặp để duyệt các giá trị có thể có của a và b, rồi tìm và gán lại tổng bé nhất a+b cho biến Smin lại sau mỗi lần duyệt
- Cách làm này học sinh chắc chắn không thể được điểm tối đa với dữ liệu
đề bài đã cho do chạy quá thời gian quy định
Do đó ta cần tìm ra cách giải tối ưu hơn
Ta có: BCNN(a,b) = suy ra a*b = BCNN(a,b)*UCLN(a,b) = G*L
Đặt T = G*L
Giả sử a<=b khi đó a <=
Mặt khác a>= G (vì G là UCLN(a,b))
Nhận xét: Tích của 2 số a, b không đổi thì tổng 2 số bé nhất khi giá trị của
a, b chênh lệch ít nhất Do đó để chương trình được thực hiện với số lần lặp ít nhất ta cho biến a chạy giảm dần từ về đến G, với mỗi giá trị của a ta tìm được giá trị của b tương ứng Tổng a, b đầu tiên được tìm thấy chính là output của bài toán
Cài đặt
Cách 1:
#include <bits/stdc++.h>
using namespace std;
int x, y, G, L;
int main()
{
Trang 6freopen("MINSUM.INP","r",stdin);
freopen("MINSUM.OUT","w",stdout);
cin >> G >> L;
int T=G*L;
int can=sqrt(T);
for(int a=can; a>=G; a )
{
if (T%a==0)
{
int b=T/a;
if( gcd(a,b)==G) {cout<<a+b; return 0;}
}
}
cout << "-1";
return 0;
}
/* Cách này học sinh chỉ ghi được điểm 85% số điểm.
Lí do: mặc dù đề bài chỉ cho G, L <=10 9 (có thể khai báo kiểu int) nhưng T= G*L <=10 18 (vượt quá phạm vi kiểu int) Do đó cách làm này sẽ cho kết quả sai do tràn bộ nhớ ở những test có dữ liệu G, L lớn (cụ thể khi G*L >10 9 )
Kết quả điểm chấm của cách làm này sẽ giúp học sinh hiểu tại sao cần phải ép kiểu, khi nào cần ép kiểu */
Cách 2:
#include <bits/stdc++.h>
using namespace std;
int x, y, G, L;
int main()
{
freopen("MINSUM.INP","r",stdin);
freopen("MINSUM.OUT","w",stdout);
cin >> G >> L;
long long T=G*L;
int can=sqrt(T);
for(int a=can; a>=G; a )
{
if (T%a=0)
Trang 7{
int b=T/a;
if( cd(a,b)==G) {cout<<a+b; return 0;}
}
}
cout << "-1";
return 0;
}
/* Ở cách làm thứ 2 này học sinh cũng chỉ ghi được 85% số điểm.
Lý do: mặc dù học sinh đã khai báo T kiểu long long nhưng trong biểu thức
T = G*L, biến G và biến L đều được khai báo kiểu int, kết quả của G*L kiểu int dẫn đến hiện tượng giá trị G*L bị tràn trước khi giá trị được đẩy vào biến T*/
Cách 3:
#include <bits/stdc++.h>
using namespace std;
int x, y, G, L;
int main()
{
freopen("MINSUM.INP","r",stdin);
freopen("MINSUM.OUT","w",stdout);
cin >> G >> L;
long long T=(long long)G*L;
int can=sqrt(T);
for(int a=can; a>=G; a )
{
if (T%a==0)
{
int b=T/a;
if( gcd(a,b)==G) {cout<<a+b; return 0;}
}
}
cout << "-1";
return 0;
}
/* Cách làm thứ 3 này học sinh ghi dc 100% số điểm, do đã giải quyết được vấn đề tràn dữ liệu và thuật toán có độ phức tạp 0(n) */
Kết quả điểm chấm của 3 cách trên như sau:
Trang 8(Đề, test chấm, code bài làm của 3 cách trên được tôi ghi vào đĩa CD, nộp kèm theo SKKN này)
Ví dụ 2: Đếm số chính phương
Số X là số chính phương khi X=a*a
(Nói cách khác giá trị là một số nguyên)
Ví dụ: 9 là số chính phương vì =3
Yêu cầu: Cho 2 số tự nhiên a, b (1 ≤ a ≤ b ≤ 1018) Hãy đếm xem trong đoạn từ a đến b có bao nhiêu số chính phương
Dữ liệu vào: Trong file DEM_CP.inp gồm 1 dòng ghi 2 số a,b
Dữ liệu ra: Trong file DEM_CP.out gồm 1 số tìm được
Test mẫu:
Giải thích test ví dụ:
Test1: a=1, b= 100 có 10 số
Test 2: a= 2, b = 10000 có 99 số
- Có 20 test ứng với a<b≤10 6
- Có 10 test ứng với a<b≤10 9
- Có 10 test ứng với a<b≤10 18
Nhận xét: vì đề bài cho a, b <=1018 nên nếu ta sử dụng 2 vòng lặp thông thường thì chắc chắn sẽ không ghi được điểm tối đa
Trang 9Do vậy ta sẽ phải tìm thuật toán tối ưu hơn,
Ý tưởng Thuật toán
Số lượng các số chính phương trong [a, b] = số lượng các số chính phương trong [1;b] – số lượng các số chính phương [1; a-1] (a-1 là số nguyên < a)
Trong đó:
- Số lượng các số chính phương trong [1 ;b] là
- Số lượng các số chính phương trong [1 ;a-1] là
Do đó: Số lượng các số chính phương trong [a, b] = –[
Cài đặt
Cách 1:
#include <bits/stdc++.h>
using namespace std;
long long a, b;
int main()
{
freopen("dem_cp.inp","r",stdin);
freopen("dem_cp.out","w", stdout);
cin >> a >> b;
cout<< trunc(sqrt(b))- trunc(sqrt(a-1));
return 0;
}
/* Với cách làm này học sinh ghi được 70-75% số điểm.
Lý do: trunc cho kết quả kiểu số thực, hiệu của hàm trunc và trunc cũng cho kết quả kiểu thực nên output đưa ra cũng là số thực.*/
Cách 2:
#include <bits/stdc++.h>
using namespace std;
long long a, b;
int main()
{
freopen("dem_cp.inp","r",stdin);
freopen("dem_cp.out","w", stdout);
cin >> a >> b;
long long m= trunc(sqrt(b))- trunc(sqrt(a-1));
cout<<m;
return 0;
}
Trang 10/* Với cách làm này học sinh ghi được 100% số điểm.
Lý do: Mặc dù trunc cho kết quả kiểu số thực, hiệu của hàm trunc và trunc cũng cho kết quả kiểu thực nhưng khi gán giá trị biểu thức trên cho biến m có kiểu long long, rồi mới in ra m thì output đưa ra là số nguyên.*/
Cách 3:
#include <bits/stdc++.h>
using namespace std;
long long a, b;
int main()
{
freopen("dem_cp.inp","r",stdin);
freopen("dem_cp.out","w", stdout);
cin >> a >> b;
cout<< (long long)trunc(sqrt(b))- (long long)trunc(sqrt(a-1));
return 0;
}
/* Với cách làm này học sinh ghi được 100% số điểm Máy tính thực hiện tính giá trị trunc(sqrt(b)) rồi ép kiểu từ kiểu số thực về kiểu long long, tính giá trị trunc(sqrt(a-1)) rồi ép kiểu từ kiểu số thực về kiểu long long Kết quả phép tính trừ của biểu thức kiểu long long với kiểu long long cũng sẽ thuộc kiểu long long.*/
Cách 4:
#include <bits/stdc++.h>
using namespace std;
long long a, b;
int main()
{
freopen("dem_cp.inp","r",stdin);
freopen("dem_cp.out","w", stdout);
cin >> a >> b;
cout<< (long long) trunc(sqrt(b)) -trunc(sqrt(a-1);
return 0;
}
/* cách làm này học sinh đã thực hiện ép kiểu cho hàm trunc(sqrt(b)) từ dạng số thực về dạng số nguyên kiểu long long Tuy nhiên hàm trunc(sqrt(a-1)) vẫn trả về kiểu số thực Kết quả hiệu của biểu thức trả về kiểu nguyên và biểu
Trang 11thức trả về kiểu thực lại vẫn trả về kiểu số thực Do vậy chương trình vẫn cho kết quả sai, học sinh sẽ không ghi được điểm tuyệt đối với cách làm này */
Cách 5
#include <bits/stdc++.h>
using namespace std;
long long a, b;
int main()
{
freopen("dem_cp.inp","r",stdin);
freopen("dem_cp.out","w", stdout);
cin >> a >> b;
long long m= trunc(sqrt(b));
long long n= trunc(sqrt(a-1));
cout<< m-n;
return 0;
}
/* Với cách làm này học sinh được điểm tuyệt đối 100% Về cơ bản cách 5 cũng gần như cách 3, nhưng việc tính trunc(sqrt(b)) và trunc(sqrt(a-1)) được thực hiện lưu giá trị vào biến m, n trước rồi mới in hiệu m-n */
Dưới đây là kết quả chấm điểm các cách của bài tập ví dụ 2
(Đề, test chấm, code bài làm của 5 cách trên được tôi ghi vào đĩa CD, nộp kèm theo SKKN này)
Ví dụ 3: Phân số - Tên file hoặc FRACTION.CPP
Khi còn bé, các bạn học sinh học được cách trừ phân số bằng cách quy đồng mẫu số, rồi mới thực hiện phép trừ
Nhưng một lần, Bờm tính thử hiệu hai phân số bằng cách lấy hiệu hai tử
số và hiệu hai mẫu số và thấy thật ngạc nhiên là kết quả vẫn đúng
Trang 12Bờm thấy tính chất này thật kỳ diệu và Bờm muốn biết, với phân số cho
trước, có bao nhiêu cặp giá trị a ≥ 0 và m > 0 sao cho:
INPUT
•Một dòng chứa hai số nguyên dương b và n cách nhau ít nhất một dấu cách (1 ≤ b, n ≤ 106)
OUTPUT
•Ghi ra một số nguyên là số lượng cặp (a, m) thỏa mãn yêu cầu.
Ví dụ:
Giải thích ví dụ:
Có 5 cặp (a, m) thỏa mãn ứng với 5 phân số:
Ý tưởng thuật toán:
Ý tưởng: Sử dụng toán học để biến đổi đẳng thức ta được
anm – an2 – bm2 + bmn = amn – bmn
– an2 = bm2 - 2bmn
an2 = 2bmn – bm2
(1)
(2)
Do a, n, b, m là các số nguyên dương nên từ (2) ta rút ra 2n > m tức là: 1<=m<2n
Từ phân tích trên ta có thể xây dựng thuật toán như sau:
Cho giá trị m chạy từ 1 đến 2n, với mỗi giá trị m thoả mãn m thì ta tìm số nguyên a tương ứng thoả mãn (1) Số cặp (a,m) tìm được chính là output của bài toán
Cài đặt
Cách 1:
#include <bits/stdc++.h>
using namespace std;
const int mmax = 1e6;
int a,m,b,n;
int d = 0;
int main()