{"version":2,"site":{"name":"CS-UIT AI Club","tagline":{"vi":"Câu lạc bộ Trí tuệ Nhân tạo","en":"Artificial Intelligence Club"},"heroHeadline":{"vi":"Chúng tôi học AI\nbằng cách xây nó.","en":"We learn AI\nby building it."},"heroBody":{"vi":"CS-UIT AI Club là cộng đồng sinh viên nghiên cứu, xây hệ thống và thi đấu về trí tuệ nhân tạo tại Khoa Khoa học Máy tính, Trường Đại học Công nghệ Thông tin — ĐHQG-HCM.","en":"CS-UIT AI Club is a student community doing AI research, engineering and competition at the Faculty of Computer Science, University of Information Technology — VNU-HCM."},"affiliation":{"vi":"Khoa Khoa học Máy tính · Trường Đại học Công nghệ Thông tin · ĐHQG-HCM","en":"Faculty of Computer Science · University of Information Technology · VNU-HCM"},"joinUrl":"","email":"","address":{"vi":"Phòng C101\nTrường Đại học Công nghệ Thông tin (UIT) — ĐHQG-HCM\nKhu phố 6, P. Linh Trung, TP. Thủ Đức, TP.HCM","en":"Room C101\nUniversity of Information Technology (UIT) — VNU-HCM\nLinh Trung Ward, Thu Duc City, Ho Chi Minh City"},"socials":{"facebook":"https://www.facebook.com/uit.cs.aiclub","github":"https://github.com/aiclub-uit","youtube":"","email":"aiclub@uit.edu.vn","group":""},"updatedAt":"2026-08-24"},"members":[{"slug":"le-dinh-duy","name":"Thầy Lê Đình Duy","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Phòng SDH&KHCN","interests":[],"avatar":{"full":"/api/uploads/63dd23be6e576c755fd1.png","thumb":"/api/uploads/61cafc8426acba139f4c.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":0},{"slug":"ngo-duc-thanh","name":"Thầy Ngô Đức Thành","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Khoa KHMT","interests":[],"avatar":{"full":"/api/uploads/3aa26fcd72fdfe836287.png","thumb":"/api/uploads/2b265ebc06ae0b1d0313.jpg"},"links":{"github":"","scholar":"","linkedin":"","email":"thanhnd@uit.edu.vn"},"order":1},{"slug":"mai-tien-dung","name":"Thầy Mai Tiến Dũng","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Khoa KHMT","interests":[],"avatar":{"full":"/api/uploads/fa499454edac278b60f3.jpg","thumb":"/api/uploads/b6ac6edf68828f00ac00.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":2},{"slug":"do-van-tien","name":"Thầy Đỗ Văn Tiến","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Khoa KHMT","interests":[],"avatar":{"full":"/api/uploads/a90a1d97a25c29526c42.jpg","thumb":"/api/uploads/5ada5804d1df5caf2234.jpg"},"links":{"github":"","scholar":"","linkedin":"","email":"tiendv@uit.edu.vn"},"order":3},{"slug":"nguyen-ngoc-thua","name":"Nguyễn Ngọc Thừa","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Phòng TN TTĐPT","interests":[],"avatar":{"full":"/api/uploads/4f08ecd2fc76c872ee03.jpg","thumb":"/api/uploads/03bfcd438e24ebcbc513.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":4},{"slug":"tran-doan-thuyen","name":"Trần Doãn Thuyên","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Khoa KHMT","interests":[],"avatar":{"full":"/api/uploads/3b01b2270ca77f4ebb51.jpg","thumb":"/api/uploads/b8156b76388e261dc1d6.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":5},{"slug":"le-tran-trong-khiem","name":"Lê Trần Trọng Khiêm","group":"advisor","role":"Cố vấn","cohort":"","faculty":"Khoa KHMT","interests":[],"avatar":{"full":"/api/uploads/13bcdaca275a73abc3a7.jpg","thumb":"/api/uploads/dc92333d53ff8fd06c76.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":6},{"slug":"tran-gia-bao","name":"Trần Gia Bảo","group":"advisor","role":"Cố vấn","cohort":"","faculty":"","interests":[],"avatar":{"full":"/api/uploads/3409673adadd5ab6e9d0.jpg","thumb":"/api/uploads/ae0e41fa0386c3da07c2.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":7},{"slug":"le-thi-thanh-tam","name":"Lê Thị Thanh Tâm","group":"member","role":"Thành viên","cohort":"KHMT2023.4","faculty":"","interests":[],"avatar":{"full":"/api/uploads/c5271dc408d8de406338.jpg","thumb":"/api/uploads/3cdbdfae681d2ef6f0a2.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":8},{"slug":"vo-tran-ngoc-vy","name":"Võ Trần Ngọc Vy","group":"member","role":"Thành viên","cohort":"KHTN2025","faculty":"","interests":[],"avatar":null,"links":{"github":"","scholar":"","linkedin":""},"order":9},{"slug":"pham-do-anh-vu","name":"Phạm Đỗ Anh Vũ","group":"member","role":"Thành viên","cohort":"KHMT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/956e40904168c3da6ea1.jpg","thumb":"/api/uploads/f33544c1ab63430a8364.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":10},{"slug":"le-hoang-hien-long","name":"Lê Hoàng Hiển Long","group":"member","role":"Thành viên","cohort":"TTNT2024","faculty":"","interests":[],"avatar":{"full":"/api/uploads/54d675922414f0898a1f.jpg","thumb":"/api/uploads/d3ce55e45fdd889a24ab.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":11},{"slug":"vo-dong-do","name":"Võ Đông Đô","group":"member","role":"Thành viên","cohort":"CNTT2025.1","faculty":"","interests":[],"avatar":{"full":"/api/uploads/c3f570e93fa881ce7206.jpg","thumb":"/api/uploads/f849fcc20a1d07604d2b.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":12},{"slug":"vo-nguyen-minh-long","name":"Võ Nguyễn Minh Long","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/18390cc42ed5c95ba515.jpg","thumb":"/api/uploads/ca7c9a9fa6848678e93a.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":13},{"slug":"hoang-ngoc-hai","name":"Hoàng Ngọc Hải","group":"member","role":"Thành viên","cohort":"KHMT2025.1","faculty":"","interests":[],"avatar":{"full":"/api/uploads/4c8d13bd2feb4f4f1601.jpg","thumb":"/api/uploads/344f01a2fb1c9dcf520c.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":14},{"slug":"doan-nguyen-minh-thu","name":"Đoàn Nguyễn Minh Thư","group":"member","role":"Thành viên","cohort":"","faculty":"","interests":[],"avatar":{"full":"/api/uploads/93eb1e7933c32f31674a.jpg","thumb":"/api/uploads/642eb7ff61b8c59c8013.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":15},{"slug":"tran-gia-huy","name":"Trần Gia Huy","group":"member","role":"Thành viên","cohort":"CNTT2025.1","faculty":"","interests":[],"avatar":{"full":"/api/uploads/9b84b8c2134a36314f6e.jpg","thumb":"/api/uploads/53e7deaf72ddcc5bd338.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":16},{"slug":"doan-le-phu-thanh","name":"Đoàn Lê Phú Thành","group":"member","role":"Thành viên","cohort":"KHDL2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/58e028bbde519e445a8b.jpg","thumb":"/api/uploads/938d1302132d5897f0f9.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":17},{"slug":"nguyen-luu-hai-anh","name":"Nguyễn Lưu Hải Anh","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/05b90b86e79749fc0e8b.jpg","thumb":"/api/uploads/46b27775a000aa83c4a5.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":18},{"slug":"nguyen-minh-chinh","name":"Nguyễn Minh Chính","group":"member","role":"Thành viên","cohort":"TTNT2024","faculty":"","interests":[],"avatar":{"full":"/api/uploads/33c791eec24e5f7a7824.jpg","thumb":"/api/uploads/810c20f483654662c429.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":19},{"slug":"pham-duy-hien","name":"Phạm Duy Hiển","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/f6ada6c1d1dc43c15dda.jpg","thumb":"/api/uploads/12803f0479c4ce7766bb.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":20},{"slug":"trinh-nguyen-cat-tuong","name":"Trịnh Nguyễn Cát Tường","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/42502a1075938719cf5e.jpg","thumb":"/api/uploads/e69bdf7a45c415befc83.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":21},{"slug":"hoang-le-kim-lam","name":"Hoàng Lê Kim Lâm","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/969580b66c3651cf4f01.jpg","thumb":"/api/uploads/f7d8de067799553e383f.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":22},{"slug":"nguyen-minh-tri","name":"Nguyễn Minh Trí","group":"member","role":"Thành viên","cohort":"KHTN2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/5e7bbf30bc67bc2754c4.jpg","thumb":"/api/uploads/deb3c897e5acbf8476d0.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":23},{"slug":"tran-lam-nguyen","name":"Trần Lâm Nguyên","group":"member","role":"Thành viên","cohort":"ATTT2025.2","faculty":"","interests":[],"avatar":{"full":"/api/uploads/729b4df0189e5e3553e2.jpg","thumb":"/api/uploads/6433c426d33d6c655fad.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":24},{"slug":"to-gia-han","name":"Tô Gia Hân","group":"member","role":"Thành viên","cohort":"CTTT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/d87cfc4fb09bf884e2cb.jpg","thumb":"/api/uploads/2b362838582b75c1ddc2.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":25},{"slug":"nguyen-khanh-van","name":"Nguyễn Khánh Vân","group":"member","role":"Thành viên","cohort":"TTNT2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/56a9fa3dadd16ccd0507.jpg","thumb":"/api/uploads/34c6d53d62bc3f33b495.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":26},{"slug":"phan-thanh-dat","name":"Phan Thành Đạt","group":"member","role":"Thành viên","cohort":"KHMT2023.1","faculty":"","interests":[],"avatar":{"full":"/api/uploads/92104c87639f55862d3e.jpg","thumb":"/api/uploads/b6b6f88877486a9f8feb.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":27},{"slug":"le-nguyen-thien-nhan","name":"Lê Nguyễn Thiện Nhân","group":"member","role":"Thành viên","cohort":"KHMT2025.3","faculty":"","interests":[],"avatar":{"full":"/api/uploads/a14662258071d318af95.jpg","thumb":"/api/uploads/5f60d88e91dadf54308b.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":28},{"slug":"do-quoc-duy","name":"Đỗ Quốc Duy","group":"member","role":"Thành viên","cohort":"TTNT2024","faculty":"","interests":[],"avatar":{"full":"/api/uploads/f9e9c2e43a1b2219dbfd.jpg","thumb":"/api/uploads/a4481a4ef911988b451d.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":29},{"slug":"duong-anh-kiet","name":"Dương Anh Kiệt","group":"member","role":"Thành viên","cohort":"KHTN2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/0fd3c01a8d9ab40a85ce.jpg","thumb":"/api/uploads/97e4af7f410cae995ed2.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":30},{"slug":"le-duc-nhan","name":"Lê Đức Nhân","group":"member","role":"Thành viên","cohort":"KHTN2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/290c2be37c48fa90be54.jpg","thumb":"/api/uploads/3fd1055936ad6a5aaab4.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":31},{"slug":"phan-trong-phuc","name":"Phan Trọng Phúc","group":"member","role":"Thành viên","cohort":"KHMT2025.3","faculty":"","interests":[],"avatar":{"full":"/api/uploads/83db36c265ff0bb2d27f.jpg","thumb":"/api/uploads/8915ea17f4e8f7a6e93c.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":32},{"slug":"phan-sy-bach","name":"Phan Sỹ Bách","group":"member","role":"Thành viên","cohort":"KHMT2025.1","faculty":"","interests":[],"avatar":{"full":"/api/uploads/3ab2f7830a202f488fb1.jpg","thumb":"/api/uploads/dfe83d749e6f549a4bfa.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":33},{"slug":"vo-hoang-kim","name":"Võ Hoàng Kim","group":"member","role":"Thành viên","cohort":"KHTN2025","faculty":"","interests":[],"avatar":{"full":"/api/uploads/fc60e2e6c3e734bf7fd0.jpg","thumb":"/api/uploads/e1f76e659ecd81d0af9d.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":34},{"slug":"do-quang-nghia","name":"Đỗ Quang Nghĩa","group":"member","role":"Thành viên","cohort":"KHMT2025.3","faculty":"","interests":[],"avatar":null,"links":{"github":"","scholar":"","linkedin":""},"order":35},{"slug":"dinh-quoc-huy","name":"Đinh Quốc Huy","group":"member","role":"Thành viên","cohort":"KHMT2025.2","faculty":"","interests":[],"avatar":{"full":"/api/uploads/9eeda13ffd3a83769f21.jpg","thumb":"/api/uploads/6a30b1486a9da9a68082.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":36},{"slug":"phan-dang-trong-tin","name":"Phan Đăng Trọng Tín","group":"member","role":"Thành viên","cohort":"KHMT2025.4","faculty":"","interests":[],"avatar":{"full":"/api/uploads/1b79f9c927a478cf6b26.jpg","thumb":"/api/uploads/49b92c4ded99df0ee751.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":37},{"slug":"nguyen-hoai-nam","name":"Nguyễn Hoài Nam","group":"alumni","role":"Chủ nhiệm CLB","cohort":"","faculty":"","interests":[],"avatar":{"full":"/api/uploads/10fdf5d97b08279fc7c4.jpg","thumb":"/api/uploads/bb2c211a64665f52380b.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":38},{"slug":"le-quang-hung","name":"Lê Quang Hưng","group":"alumni","role":"Thành viên","cohort":"","faculty":"","interests":[],"avatar":{"full":"/api/uploads/25a7e0aa65ea7c803343.jpg","thumb":"/api/uploads/0aaa1da6b54576f819a0.jpg"},"links":{"github":"","scholar":"","linkedin":""},"order":39}],"publications":[{"id":"a-two-stage-refinement-framework-for-robust-vehicle-detection-in-traff","title":"A Two-Stage Refinement Framework for Robust Vehicle Detection in Traffic Surveillance","authors":["Nguyen, K.","Van, H.T.","Thien, N.N.V.","Thanh, D.P.","Do, T.","Ngo, T.D."],"venue":"CITA","year":2026,"area":"Computer Vision","tags":["Deep Learning","PyTorch","Vehicle Detection"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=A+Two-Stage+Refinement+Framework+for+Robust+Vehicle+Detection+in+Traffic+Surveillance","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"nii-uit-at-vbs2026-towards-effective-visual-question-answering-for-int","title":"NII-UIT at VBS2026: Towards Effective Visual Question Answering for Interactive and Multimodal Video Retrieval","authors":["Bao Tran","Tien Do","Thanh Duc Ngo","Duy-Dinh Le","Shin’ichi Satoh"],"venue":"","year":2026,"area":"Video Retrieval","tags":[],"paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","state":"completed","featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"When Helpful Text Hurts: Option-Redirecting Bias in Vision–Language Models","id":"option-redirecting-bias-vlm","authors":["Tam Le Thi Thanh","Hoang Tran Van","Thanh Duc Ngo"],"venue":"ACM MM","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"From Dialogue to Evidence: Retrieval-State-Conditioned Interaction for Text-Based Person Retrieval","id":"dialogue-to-evidence-interactive-person-retrieval","authors":["Bao Tran","Thanh Duc Ngo"],"venue":"ACM MM","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Towards Scalable and Context-Aware Multimodal Interactive Video Retrieval","id":"scalable-context-aware-multimodal-interactive-video-retrieval","authors":["Bao Tran","Khiem Le","Thanh Duc Ngo"],"venue":"Multimedia Systems","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"ITSELF: Attention Guided Fine-Grained Alignment for Vision-Language Retrieval","id":"itself-vision-language-retrieval","authors":["Tien-Huy Nguyen","Huu-Loc Tran","Thanh Duc Ngo"],"venue":"WACV","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"https://trhuuloc.github.io/itself","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"MVDGC: Joint 3D and 2D Multi-view Pedestrian Detection via Dual Geometric Constraints","id":"mvdgc-joint-3d-and-2d-multi-view-pedestrian-detection-via-dual-geometr","authors":["Thinh Phan","Hao Vo","Khoa Vo","Cuong Pham","Thanh Duc Ngo","Ngan Le"],"venue":"TMLR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Holistic Feature Fusion for Fine-Grained Pen Classification","id":"holistic-feature-fusion-for-fine-grained-pen-classification","authors":["Huy Nguyen Pham Gia","Dang Ngo Viet Tue","Hien Pham Duy","Bao Ta Cao Nguyen","Thuyen Tran Doan","Thanh Duc Ngo"],"venue":"MAPR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"MGTE: A Modular Multi-Granularity Text Ensemble for Interactive Image Retrieval","id":"mgte-a-modular-multi-granularity-text-ensemble-for-interactive-image-r","authors":["Khiem Le","Bao Tran","Duy-Dinh Le","Thanh Duc Ngo"],"venue":"MAPR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Super-Image Reranking: Lightweight Temporal Context Aggregation for Ad-hoc Video Search","id":"super-image-reranking-lightweight-temporal-context-aggregation-for-ad-","authors":["Thuyen Tran Doan","Tien Do","Thanh Duc Ngo"],"venue":"MAPR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Track-Level Aggregation for Low-Resolution License Plate Recognition","id":"track-level-aggregation-for-low-resolution-license-plate-recognition","authors":["Khanh V. Vu Nguyen","Sang T. Pham","Duc N. N. Phung","Trong P. Le","Vy N. Vo Tran","Tien Do","Duy-Dinh Le"],"venue":"MAPR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Segmentation-Guided Scene Perturbation for Passage-Based Top-View Person Re-Identification","id":"segmentation-guided-scene-perturbation-for-passage-based-top-view-pers","authors":["Hien Pham Duy","Bao Tran","Tien Do","Duy-Dinh Le","Thanh Duc Ngo"],"venue":"MAPR","year":2026,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"a-hybrid-video-retrieval-system-using-clip-and-beit-3-for-enhanced-und","title":"A Hybrid Video Retrieval System Using CLIP and BEiT-3 for Enhanced Understanding","authors":["Long, L. B.","Phong, N. H.","Thang, N. T.","et al."],"venue":"Chapter Publication","year":2025,"area":"Video Retrieval","tags":["CLIP","BEiT-3","Hybrid Search"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=A+Hybrid+Video+Retrieval+System+Using+CLIP+and+BEiT-3+for+Enhanced+Understanding","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"a-lightweight-and-data-centric-framework-for-real-time-object-detectio","title":"A Lightweight and Data-Centric Framework for Real-Time Object Detection in Fisheye Camera","authors":["Vinh, A. T.","et al."],"venue":"ICCV","year":2025,"area":"Computer Vision","tags":["Real-time","Fisheye Camera","Lightweight"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=A+Lightweight+and+Data-Centric+Framework+for+Real-Time+Object+Detection+in+Fisheye+Camera","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"context-aware-question-answering-for-vietnamese-university-admissions-","title":"Context-Aware Question Answering for Vietnamese University Admissions via Multi-LLM Architecture","authors":["Thanh, T. L. T.","Trong, H. H.","Tran Gia, B.","Nguyen, T.","Do, T.","Ngo, T. D.","Nguyen, N. L. T."],"venue":"MAPR","year":2025,"area":"NLP","tags":["QA","Multi-LLM","Context-Aware"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Context-Aware+Question+Answering+for+Vietnamese+University+Admissions+via+Multi-LLM+Architecture","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"efficient-and-distortion-aware-fisheye-object-detection-for-edge-devic","title":"Efficient and Distortion-Aware Fisheye Object Detection for Edge Devices","authors":["Gia, B. T.","Khanh, T. B. C.","Le Thi Thanh, T.","Trong, H. H.","Do, T.","Ngo, T. D.","Le, D. D."],"venue":"ICCV","year":2025,"area":"Computer Vision","tags":["Fisheye Detection","Edge AI","Optimization"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Efficient+and+Distortion-Aware+Fisheye+Object+Detection+for+Edge+Devices","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"nii-uit-at-vbs2025-multimodal-video-retrieval-with-llm-integration-and","title":"NII-UIT at VBS2025: Multimodal Video Retrieval with LLM Integration and Dynamic Temporal Search","authors":["Tran Gia, B.","Khanh, T. B. C.","Thanh, T. L. T.","Doan, T. T.","Le, K.","Do, T.","Mai, T. D.","Ngo, T. D."],"venue":"MMM","year":2025,"area":"Video Retrieval","tags":["Multimodal","LLM","Temporal Search"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=NII-UIT+at+VBS2025+Multimodal+Video+Retrieval+with+LLM+Integration+and+Dynamic+Temporal+Search","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"robust-traffic-vehicle-detection-with-misclassified-vehicles-minimizat","title":"Robust Traffic Vehicle Detection with Misclassified Vehicles Minimization and Weighted Box Fusion","authors":["Minh, T. H. M.","Le Bao, L.","Phat, N. T.","et al."],"venue":"MAPR","year":2025,"area":"Computer Vision","tags":["Vehicle Detection","Weighted Box Fusion"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Robust+Traffic+Vehicle+Detection+with+Misclassified+Vehicles+Minimization+and+Weighted+Box+Fusion","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"skeleton-guided-artistic-text-recognition","title":"Skeleton-Guided Artistic Text Recognition","authors":["Do, T.","Tran, T.","Le, K.","Ngo, T. D."],"venue":"IJDAR","year":2025,"area":"NLP & OCR","tags":["Artistic OCR","Deep Learning","Skeleton-Guided"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Skeleton-Guided+Artistic+Text+Recognition","codeUrl":"https://github.com/aiclub-uit/Skeleton-Guided-Artistic-Text-Recognition","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"towards-understanding-the-logical-layout-of-scene-text-in-signboard-im","title":"Towards Understanding the Logical Layout of Scene Text in Signboard Images","authors":["Giang, T. T. C.","Cam-Nguyen, T.-N.","et al."],"venue":"Chapter Publication","year":2025,"area":"NLP & OCR","tags":["Scene Text","Logical Layout"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Towards+Understanding+the+Logical+Layout+of+Scene+Text+in+Signboard+Images","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"unveiling-peripheral-information-a-context-aware-video-retrieval-appro","title":"Unveiling Peripheral Information: A Context-Aware Video Retrieval Approach","authors":["Quan, N. H.","Nhan, N. V. T.","et al."],"venue":"Chapter Publication","year":2025,"area":"Video Retrieval","tags":["Clue Retrieval","Context-Aware"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Unveiling+Peripheral+Information+A+Context-Aware+Video+Retrieval+Approach","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"vrag-retrieval-augmented-video-question-answering-for-long-form-videos","title":"VRAG: Retrieval-Augmented Video Question Answering for Long-Form Videos","authors":["Tran Gia, B.","Le, K.","Do, T.","Mai, T. D.","Ngo, T. D.","Le, D. D.","Satoh, S."],"venue":"CVPRW","year":2025,"area":"CV & NLP","tags":["Video QA","RAG","LLM"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=VRAG+Retrieval-Augmented+Video+Question+Answering+for+Long-Form+Videos","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"zero-shot-artistic-text-recognition-with-multimodal-language-models","title":"Zero-shot Artistic Text Recognition with Multimodal Language Models","authors":["Do, T.","Tran, T.","Le, D.-D.","Ngo, T. D."],"venue":"APSIPA ASC","year":2025,"area":"NLP & OCR","tags":["Zero-shot","Multimodal","LLM"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Zero-shot+Artistic+Text+Recognition+with+Multimodal+Language+Models","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Few-Shot Instance Segmentation: An Exploration in the Frequency Domain for Camouflage Instances","id":"few-shot-instance-segmentation-an-exploration-in-the-frequency-domain-","authors":["Thanh-Danh Nguyen","Hung-Phu Cao","Thanh Duc Ngo","Vinh-Tiep Nguyen","Tam V. Nguyen"],"venue":"MAPR","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"https://github.com/danhntd/FS-CAMOFreq","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"UDP-Edit: Union Dual-Prompt Attention for Local Image Editing in Fast Diffusion Models","id":"udp-edit-union-dual-prompt-attention-for-local-image-editing-in-fast-d","authors":["Trong-Tai Dam Vu","Vinh-Tiep Nguyen","Van Kiet Nguyen","Thanh Duc Ngo","Duy-Dinh Le","Ngan Luu-Thuy Nguyen"],"venue":"MAPR","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Can LLMs Play Ô Ăn Quan? A Study of Multi-Step Planning and Decision Making","id":"can-llms-play-o-an-quan-a-study-of-multi-step-planning-and-decision-ma","authors":["Sang Quang Nguyen","Kiet Van Nguyen","Vinh-Tiep Nguyen","Thanh Duc Ngo","Ngan Luu-Thuy Nguyen","Duy-Dinh Le"],"venue":"MAPR","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"CAMUL: Context-Aware Multi-conditional Instance Synthesis for Image Segmentation","id":"camul-context-aware-multi-conditional-instance-synthesis-for-image-seg","authors":["Thanh-Danh Nguyen","Trong-Tai Dam Vu","Bich-Nga Pham","Thanh Duc Ngo","Tam V. Nguyen","Vinh-Tiep Nguyen"],"venue":"IEEE MultiMedia","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Stratified Domain Adaptation: A Progressive Self-Training Approach for Scene Text Recognition","id":"stratified-domain-adaptation-str","authors":["Kha Nhat Le","Hoang-Tuan Nguyen","Hung Tien Tran","Thanh Duc Ngo"],"venue":"WACV","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"https://github.com/KhaLee2307/StrDA","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Multi-Perspective Data Augmentation for Few-shot Object Detection","id":"mpad-few-shot-object-detection","authors":["Anh-Khoa Nguyen Vu","Quoc-Truong Truong","Vinh-Tiep Nguyen","Thanh Duc Ngo","Thanh-Toan Do","Tam V. Nguyen"],"venue":"ICLR","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"https://github.com/nvakhoa/MPAD","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Generalizable Sign Language Recognition via Local Temporal Convolutions and Region-Aware Pose Encoding","id":"generalizable-sign-language-recognition-via-local-temporal-convolution","authors":["Sieu Tran","Duc Nguyen Minh","Truong Nguyen Thanh","Hao Vo","Thanh Duc Ngo","Tien Do","Khiem Le","Duy-Dinh Le"],"venue":"ICCVW","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Region-Aware Pose Modeling and Permutation Decoding for Signer-Independent Sign Language Recognition","id":"region-aware-pose-modeling-and-permutation-decoding-for-signer-indepen","authors":["Sieu Tran","Duc Nguyen Minh","Truong Nguyen Thanh","Hao Vo","Thanh Duc Ngo","Tien Do","Khiem Le","Duy-Dinh Le"],"venue":"ICCVW","year":2025,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"addressing-ambiguous-queries-in-video-retrieval-with-advanced-temporal","title":"Addressing Ambiguous Queries in Video Retrieval with Advanced Temporal Search","authors":["Bao Tran Gia","et al."],"venue":"SOICT","year":2024,"area":"Video Retrieval","tags":["Temporal Search","Ambiguous Queries"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Addressing+Ambiguous+Queries+in+Video+Retrieval+with+Advanced+Temporal+Search","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"enhancing-road-object-detection-in-fisheye-cameras-sahi-and-hybrid-inf","title":"Enhancing Road Object Detection in Fisheye Cameras: SAHI and Hybrid Inference","authors":["Tran Gia, B.","Khanh, T. B. C.","Trong, H. H.","Doan, T. T.","Do, T.","Le, D. D.","Ngo, T. D."],"venue":"CVPRW","year":2024,"area":"Computer Vision","tags":["SAHI","Hybrid Inference","Road Detection"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Enhancing+Road+Object+Detection+in+Fisheye+Cameras+SAHI+and+Hybrid+Inference","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"key-information-extraction-and-recognition-from-rich-text-images","title":"Key Information Extraction and Recognition from Rich Text Images","authors":["Do, T.","Tran Doan, T.","Le, K.","Ngo, T. D."],"venue":"Vietnam Journal of Computer Science","year":2024,"area":"NLP & OCR","tags":["KIE","RoBERTa","LION Optimizer"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Key+Information+Extraction+and+Recognition+from+Rich+Text+Images","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"robust-motorcycle-helmet-detection-using-co-detr-and-minority-class-en","title":"Robust Motorcycle Helmet Detection using Co-DETR and Minority Class Enhancement","authors":["Vo, H.","Sieu Tran","Duc Minh Nguyen","et al."],"venue":"CVPRW","year":2024,"area":"Computer Vision","tags":["Co-DETR","Helmet Detection","Safety"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Robust+Motorcycle+Helmet+Detection+using+Co-DETR+and+Minority+Class+Enhancement","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"signboardtext-text-detection-and-recognition-in-in-the-wild-signboard-","title":"SignboardText: Text Detection and Recognition in In-the-Wild Signboard Images","authors":["Tien Do","Thuyen Tran","et al."],"venue":"IEEE Access","year":2024,"area":"NLP & OCR","tags":["IEEE Access","Text Spotting","In-the-wild"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=SignboardText+Text+Detection+and+Recognition+in+In-the-Wild+Signboard+Images","codeUrl":"https://github.com/aiclub-uit/SignboardText","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Event retrieval from large video collection in Ho Chi Minh City AI challenge 2024","id":"event-retrieval-from-large-video-collection-in-ho-chi-minh-city-ai-cha","authors":["Trong-Le Do","Viet-Tham Huynh","Thuc Nguyen-Quang","Hai-Dang Nguyen","Mai-Khiem Tran","Tu V. Ninh","Tu-Khiem Le","Thanh Duc Ngo","Duc-Tien Dang-Nguyen","Tu-Trinh Ngo","Klaus Schöffmann","Cathal Gurrin","Minh-Triet Tran"],"venue":"SOICT","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"InstSynth: Instance-wise Prompt-guided Style Masked Conditional Data Synthesis for Scene Understanding","id":"instsynth-instance-wise-prompt-guided-style-masked-conditional-data-sy","authors":["Thanh-Danh Nguyen","Bich-Nga Pham","Trong-Tai Dam Vu","Vinh-Tiep Nguyen","Thanh Duc Ngo","Tam V. Nguyen"],"venue":"MAPR","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Controllable Base Class Synthesis with Generative Diffusion Model for Few-Shot Object Detection","id":"controllable-base-class-synthesis-with-generative-diffusion-model-for-","authors":["Quoc-Truong Truong","Anh-Khoa Nguyen Vu","Vinh-Tiep Nguyen","Thanh Duc Ngo"],"venue":"MAPR","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Contrastive Learning with Weakly Pair Images for Traffic Image Deraining","id":"contrastive-learning-with-weakly-pair-images-for-traffic-image-deraini","authors":["Quang Minh Tran","Thanh Duc Ngo","Tien-Dung Mai"],"venue":"MAPR","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Weakly Supervised Object Detection Using Class Activation Map","id":"weakly-supervised-object-detection-using-class-activation-map","authors":["Vinh Phu Tran","Tuan Tran Anh Dao","Tien-Dung Mai","Thanh Duc Ngo"],"venue":"MAPR","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"Intrinsic Motivational States Can Be Classified by Non-Contact Measurement of Autonomic Nervous System Activation and Facial Expressions","id":"intrinsic-motivational-states-can-be-classified-by-non-contact-measure","authors":["Sae Kawasaki","Koichi Ashida","Vinh-Tiep Nguyen","Thanh Duc Ngo","Duy-Dinh Le","Hirokazu Doi","Norimichi Tsumura"],"venue":"Applied Sciences","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"title":"The art of camouflage: Few-shot learning for animal detection and segmentation","id":"the-art-of-camouflage-few-shot-learning-for-animal-detection-and-segme","authors":["Thanh-Danh Nguyen","Anh-Khoa Nguyen Vu","Nhat-Duy Nguyen","Vinh-Tiep Nguyen","Thanh Duc Ngo","Thanh-Toan Do","Minh-Triet Tran","Tam V. Nguyen"],"venue":"IEEE Access","year":2024,"area":"","state":"published","paperUrl":"","scholarUrl":"","codeUrl":"","projectUrl":"","tags":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"diverse-search-methods-and-multi-modal-fusion-for-high-performance-vid","title":"Diverse Search Methods and Multi-Modal Fusion for High-Performance Video Retrieval","authors":["Sieu Tran","et al."],"venue":"SOICT","year":2023,"area":"Video Retrieval","tags":["Multi-Modal Fusion","Video Retrieval"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Diverse+Search+Methods+and+Multi-Modal+Fusion+for+High-Performance+Video+Retrieval","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"integrating-multiple-models-for-effective-video-retrieval-and-multi-st","title":"Integrating Multiple Models for Effective Video Retrieval and Multi-stage Search","authors":["Tran Gia, B.","Bui Cong Khanh, T.","et al."],"venue":"SOICT","year":2023,"area":"Video Retrieval","tags":["Multi-stage Search","Ensemble Models"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Integrating+Multiple+Models+for+Effective+Video+Retrieval+and+Multi-stage+Search","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"masked-face-recognition-using-eum-feature-extraction-from-unobstructed","title":"Masked Face Recognition Using EUM Feature Extraction from Unobstructed Region","authors":["Thuyen Tran Doan","et al."],"venue":"MAPR","year":2023,"area":"Computer Vision","tags":["Face Recognition","Feature Extraction"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Masked+Face+Recognition+Using+EUM+Feature+Extraction+from+Unobstructed+Region","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"roberta-ensemble-technique-for-document-information-localization-and-e","title":"RoBERTa Ensemble Technique for Document Information Localization and Extraction","authors":["Tran, B. G.","Bao, D. N. M.","et al."],"venue":"CLEF Working Notes","year":2023,"area":"NLP","tags":["RoBERTa","Ensemble","Document AI"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=RoBERTa+Ensemble+Technique+for+Document+Information+Localization+and+Extraction","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"a-framework-for-evaluating-video-summary-approaches","title":"A Framework for Evaluating Video Summary Approaches","authors":["Mai, T.-D.","Do, T.","Le, D.-D."],"venue":"MAPR","year":2022,"area":"Video Processing","tags":["Video Summarization","Evaluation Framework"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=A+Framework+for+Evaluating+Video+Summary+Approaches","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"uit-at-vbs-2022-an-unified-and-interactive-video-retrieval-system-with","title":"UIT at VBS 2022: An Unified and Interactive Video Retrieval System with Temporal Search","authors":["Chung Duc, K. H.","et al."],"venue":"LNCS","year":2022,"area":"Video Retrieval","tags":["VBS 2022","Interactive Search"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=UIT+at+VBS+2022+An+Unified+and+Interactive+Video+Retrieval+System+with+Temporal+Search","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"mc-ocr-challenge-2021-an-end-to-end-vietnamese-receipts-recognition-fr","title":"MC-OCR Challenge 2021: An end-to-end Vietnamese Receipts Recognition Framework","authors":["Le, H.","To, A.","et al."],"venue":"RIVF","year":2021,"area":"NLP & OCR","tags":["Receipt OCR","RIVF 2021"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=MC-OCR+Challenge+2021+An+end-to-end+Vietnamese+Receipts+Recognition+Framework","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"unweighted-bipartite-matching-for-robust-vehicle-counting","title":"Unweighted Bipartite Matching For Robust Vehicle Counting","authors":["Chung Duc, K. H.","et al."],"venue":"MAPR","year":2021,"area":"Computer Vision","tags":["Vehicle Counting","Tracking"],"paperUrl":"","scholarUrl":"https://scholar.google.com/scholar?q=Unweighted+Bipartite+Matching+For+Robust+Vehicle+Counting","codeUrl":"","projectUrl":"","state":"published","featured":false,"memberSlugs":[],"projectSlugs":[]}],"awards":[{"id":"top-view-person-re-identification-icpr-2026-2026","competition":"Top-View Person Re-Identification - ICPR 2026","placement":"Giải Nhất","rank":1,"year":2026,"month":"","scope":"international","domain":"Computer Vision","organizer":"International Conference on Pattern Recognition 2026","description":"CHÚC MỪNG sinh viên năm nhất ngành Trí tuệ Nhân tạo – CLB AI, Khoa Khoa học Máy tính – UIT đã đạt TOP 1 tại cuộc thi thuộc khuôn khổ hội nghị ICPR – Track RGB (TVRID Challenge) 🎉\n\n🎯 Cuộc thi xoay quanh bài toán TVRID (Top-View Person Re-Identification) – định danh lại người từ góc nhìn trên cao, một hướng nghiên cứu khó trong lĩnh vực thị giác máy tính, có ứng dụng lớn trong giám sát thông minh tại siêu thị, sân bay, nhà ga và hệ thống đa camera, đồng thời hỗ trợ bảo vệ quyền riêng tư.","externalUrl":"","images":[{"full":"/api/uploads/3368d6ce08c267579e14.jpg","thumb":"/api/uploads/6496aa600e88b0412c1d.jpg"}],"featured":true,"memberSlugs":[],"projectSlugs":[]},{"id":"icdar-2026-circleid-pen-classification-2026","competition":"ICDAR 2026 – CircleID Pen Classification","placement":"Giải Nhất","rank":1,"year":2026,"month":"","scope":"international","domain":"Document AI & OCR","organizer":"ICDAR 2026","description":"Cuộc thi tập trung vào bài toán phân loại nét bút và nhận dạng đặc trưng chữ viết tay, thu hút nhiều đội thi quốc tế tham gia nghiên cứu và phát triển các phương pháp trong lĩnh vực Computer Vision và Machine Learning.","externalUrl":"","images":[{"full":"/api/uploads/483e53eb3b56e2a20ca7.jpg","thumb":"/api/uploads/bba7725ebbf08b4adc7f.jpg"}],"featured":true,"memberSlugs":[],"projectSlugs":[]},{"id":"icpr-2026-competition-on-low-resolution-license-plate-recognition-2026","competition":"ICPR 2026 Competition on Low-Resolution License Plate Recognition","placement":"Top 5","rank":5,"year":2026,"month":"","scope":"international","domain":"Computer Vision","organizer":"ICPR 2026","description":"Cuộc thi là một phần của hội nghị ICPR – International Conference on Pattern Recognition, quy tụ 99 đội nghiên cứu AI từ các trường đại học và phòng lab trên khắp thế giới, cùng giải quyết bài toán khó trong lĩnh vực Computer Vision và OCR.","externalUrl":"","images":[{"full":"/api/uploads/2f1a028143a50aba97fc.jpg","thumb":"/api/uploads/2110cd9db9c9c3fd5826.jpg"}],"featured":true,"memberSlugs":[],"projectSlugs":[]},{"id":"ky-thi-olympic-tri-tue-nhan-tao-sinh-vien-viet-nam-lan-thu-i-nam-2025-","competition":"Kỳ thi Olympic Trí tuệ Nhân tạo Sinh viên Việt Nam lần thứ I, năm 2025 (OLPAI 2025)","placement":"Giải Nhất, Giải Ba","rank":1,"year":2025,"month":"12","scope":"national","domain":"General AI","organizer":"","description":"","externalUrl":"","images":[{"full":"/api/uploads/b68276f44a1d34d7d32a.jpg","thumb":"/api/uploads/5dcdb324d2015d02a4c5.jpg"},{"full":"/api/uploads/6c66ac3f59ce9e6d649e.jpg","thumb":"/api/uploads/7430a098b35e192721dd.jpg"}],"featured":true,"memberSlugs":[],"projectSlugs":[]},{"id":"hoi-thi-thu-thach-tri-tue-nhan-tao-tp-ho-chi-minh-nam-2025-2025","competition":"Hội thi Thử thách Trí tuệ Nhân tạo TP. Hồ Chí Minh năm 2025","placement":"Giải Nhất, Giải Nhì, Giải Ba","rank":1,"year":2025,"month":"11","scope":"local","domain":"General AI","organizer":"","description":"Xin chúc mừng các bạn đến từ Câu lạc bộ AI - khoa Khoa học Máy tính UIT Khoa Khoa học Máy tính UIT\nĐội đã đạt thành tích xuất sắc với:\n🏆 1 GIẢI NHẤT — OpenCubee_1\n🥈 1 GIẢI NHÌ — OpenCubee_2 \n🥉 1 GIẢI BA — LunchRetrieval\n🎖️ 1 GIẢI KHUYẾN KHÍCH: Your Answer\n🚀 2 GIẢI TIỀM NĂNG:\n• DinnerRetrieval\n• BUCCI_GANG \n Vượt qua 680 đội thi đến từ 131 đơn vị (các trường đại học và doanh nghiệp), với tổng cộng 3.198 thí sinh tham gia bảng A – dành cho sinh viên tại Hội thi Thử thách Trí tuệ Nhân tạo TP. Hồ Chí Minh năm 2025.\nHội thi là sân chơi công nghệ uy tín do Sở Khoa học và Công nghệ TP.HCM phối hợp cùng các đơn vị học thuật và tổ chức chuyên môn triển khai, hướng đến thúc đẩy nghiên cứu, sáng tạo và ứng dụng AI trong các bài toán thực tiễn của đô thị thông minh. Năm 2025, đề bài tập trung vào việc xây dựng Trợ lý ảo thông minh có khả năng phân tích, tìm kiếm và truy xuất thông tin từ kho dữ liệu multimedia lớn, mô phỏng theo tiêu chuẩn của các cuộc thi quốc tế như LSC và VBS. Đây là môi trường cạnh tranh chất lượng cao, nơi sinh viên được thử thách kỹ năng AI toàn diện và có cơ hội tiếp cận cộng đồng chuyên gia – doanh nghiệp trong lĩnh vực.\nĐÂY LÀ LẦN THỨ 4 LIÊN TIẾP Câu lạc bộ AI - khoa Khoa học Máy tính UIT NHẤT TẠI CUỘC THI NÀY \nMột lần nữa xin gửi lời chúc mừng đến tập thể các bạn vì những nỗ lực và thành tích rất đáng tự hào! 🚀🤖","externalUrl":"","images":[{"full":"/api/uploads/b3b128d2478b99cd4291.jpg","thumb":"/api/uploads/89e98282d5c18df50e4e.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"video-browser-showdown-vbs-2025-2025","competition":"Video Browser Showdown (VBS 2025)","placement":"Best Overall System","rank":1,"year":2025,"month":"1","scope":"international","domain":"Video Understanding","organizer":"","description":"Chúc mừng đội NII_UIT\nĐội NII_UIT, gồm các thành viên đến từ Câu lạc bộ AI - khoa Khoa học Máy tính UIT  Trường Đại học Công nghệ Thông tin, phối hợp cùng Viện Thông tin Quốc gia Nhật Bản, đã xuất sắc giành giải Nhất tại cuộc thi VBS 2025 (Video Browser Showdown). Đây là cuộc thi tìm kiếm video trên tập dữ liệu lớn, được tổ chức thường niên cùng với hội nghị MMM 2025 (Multimedia Modelling) – một hội nghị uy tín trong lĩnh vực đa phương tiện, lần thứ 31 diễn ra tại Nhật Bản.\nGiới thiệu về VBS 2025 (Video Browser Showdown)\nVideo Browser Showdown (VBS) là một cuộc thi quốc tế được tổ chức hàng năm, nơi các nhóm nghiên cứu và phát triển trên thế giới trình diễn các hệ thống tìm kiếm video tiên tiến. VBS 2025 có sự tham gia của hơn 17 đội thi đến từ nhiều trường đại học hàng đầu thế giới. Cuộc thi tiếp tục là sân chơi thúc đẩy đổi mới trong lĩnh vực truy xuất thông tin đa phương tiện, với mục tiêu phát triển các công cụ mạnh mẽ, trực quan, giúp người dùng tìm kiếm nội dung video nhanh chóng và chính xác trên tập dữ liệu khổng lồ hơn 9TB.\nThách thức trong VBS 2025\n- Tìm kiếm nội dung phức tạp: Đòi hỏi xử lý truy vấn bao gồm ngôn ngữ tự nhiên, hình ảnh hoặc video mẫu, và kết hợp thông tin từ nhiều nguồn hiệu quả.\nKhả năng phản hồi thời gian thực: Các đội phải tối ưu hóa để đưa ra kết quả nhanh chóng trên tập dữ liệu hơn 9TB.\n- Hiểu ngữ cảnh đa phương tiện: Phải nhận diện vật thể, cảnh quan, hành động hoặc chi tiết nhỏ một cách chính xác.\n- Đa dạng ngôn ngữ và văn hóa: Hệ thống cần linh hoạt xử lý truy vấn thuộc nhiều ngôn ngữ và bối cảnh văn hóa khác nhau.\n- Giao diện người dùng: Hệ thống phải trực quan, dễ sử dụng để hỗ trợ người dùng thao tác hiệu quả.\nVBS 2025 không chỉ là nơi so tài công nghệ mà còn là cơ hội để các nhóm học hỏi, giao lưu, và thúc đẩy sự phát triển ứng dụng trí tuệ nhân tạo trong lĩnh vực truy xuất thông tin đa phương tiện.","externalUrl":"https://arxiv.org/abs/2509.12000","images":[{"full":"/api/uploads/6f1f87d5fa45428758e0.jpg","thumb":"/api/uploads/8c1cfd6425973d7ecb16.jpg"},{"full":"/api/uploads/3275c35206ca08940fe2.jpg","thumb":"/api/uploads/6cdf434fd3383c6a7fe1.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"trecvid-2025-2025","competition":"TRECVID 2025","placement":"Giải Nhất (task VQA Multiple Choice)","rank":1,"year":2025,"month":"9","scope":"international","domain":"Video Understanding","organizer":"","description":"Đạt thứ hạng cao nhất trong bài toán Video Question Answering quy mô toàn cầu.","externalUrl":"","images":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"giai-thuong-khoa-hoc-cong-nghe-danh-cho-sinh-vien-trong-cac-co-so-giao","competition":"Giải thưởng Khoa học Công nghệ dành cho sinh viên trong các cơ sở giáo dục đại học năm 2025","placement":"Giải Nhất","rank":1,"year":2025,"month":"11","scope":"national","domain":"","organizer":"Bộ Giáo dục & Đào tạo","description":"Công nhận những nỗ lực xuất sắc trong nghiên cứu khoa học cấp quốc gia.","externalUrl":"","images":[{"full":"/api/uploads/c7ac065ca9e937ce7236.jpg","thumb":"/api/uploads/5d8f54fa1feff5e55e41.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"olympic-tri-tue-nhan-tao-tp-hcm-2025-oai-hcmc-2025-2025","competition":"Olympic Trí tuệ nhân tạo TP.HCM 2025 (OAI HCMC 2025)","placement":"Giải Nhì","rank":2,"year":2025,"month":"4","scope":"local","domain":"General AI","organizer":"","description":"","externalUrl":"","images":[{"full":"/api/uploads/d47564692048df9c306d.jpg","thumb":"/api/uploads/634e03f7a7440a4c8e0d.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"thiet-ke-dien-tu-viet-nam-2025-2025","competition":"Thiết kế điện tử Việt Nam 2025","placement":"Giải Ba","rank":3,"year":2025,"month":"12","scope":"national","domain":"Embedded & Electronics","organizer":"","description":"","externalUrl":"","images":[{"full":"/api/uploads/6e8b2a2e7b90f28fbfcf.jpg","thumb":"/api/uploads/42bd0594767e0cacb9e5.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"viettel-ai-race-2025-2025","competition":"Viettel AI Race 2025","placement":"Giải Khuyến Khích","rank":9,"year":2025,"month":"9","scope":"national","domain":"General AI","organizer":"Tập đoàn Viettel","description":"Thử thách giải quyết các bài toán AI thực tế quy mô lớn.","externalUrl":"","images":[{"full":"/api/uploads/e75a9c07019a11337004.jpg","thumb":"/api/uploads/236170b7ff3fd7676448.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"trecvid-nist-2024","competition":"TRECVID (NIST)","placement":"Giải Nhất","rank":1,"year":2024,"month":"","scope":"international","domain":"Video Understanding","organizer":"","description":"Tiếp tục khẳng định năng lực tại đấu trường NIST danh giá năm 2024.","externalUrl":"","images":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"ai-city-challenge-nvidia-cvpr-2024-2024","competition":"AI City Challenge (NVIDIA/CVPR) 2024","placement":"Giải Nhất (Track 5); Top 4 (Track 4)","rank":1,"year":2024,"month":"6","scope":"international","domain":"Computer Vision","organizer":"NVIDIA & CVPR","description":"Chúc mừng các bạn Võ Anh Hào, Trần Siêu, Nguyễn Minh Đức các thành viên của đội Helios đến từ Câu lạc bộ AI - khoa Khoa học Máy tính UIT đã chính thức được công bố là đội đạt được TOP 1 ( trong hơn 60 đội thi đến từ các Trường, Viện, công ty công nghệ của thế giới) tại Track 5 Detecting Violation of Helmet Rule for Motorcyclists( Phát hiện vi phạm quy định về đội mũ bảo hiểm đối với người đi xe máy trong video) -  tại cuộc thi 2024 AI CITY CHALLENGE  trong khuôn khổ của hội nghị CVPR 2024 (Hội nghị chuyên ngành uy tín nhất trong lĩnh vực thị giác máy tính) và NVIDIA tổ chức 😍😍\nChúc mừng các bạn Trần Gia Bảo, Bùi Công Khánh Tường, Hồ Trọng Hiển đến từ Câu lạc bộ AI - khoa Khoa học Máy tính UIT đã đạt được top 4 (với hơn 52 đội thi đến từ các công ty công nghệ, Trường Đại Học, Viện đào tạo trên thế giới) trong cuộc thi 2024 AI CITY CHALLENGE với bài toán về nhận dạng vật thể từ camera giao thông (Road Object Detection in Fish-Eye Cameras) được tổ chức trong khuôn khổ hội nghị CVPR 2024.","externalUrl":"https://www.aicitychallenge.org/2024-challenge-winners/","images":[],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"hoi-thi-thu-thach-tri-tue-nhan-tao-tp-ho-chi-minh-nam-2024-2024","competition":"Hội thi Thử thách Trí tuệ Nhân tạo TP. Hồ Chí Minh năm 2024","placement":"Giải Nhất; Giải Nhì; Giải Khuyến Khích","rank":1,"year":2024,"month":"12","scope":"local","domain":"General AI","organizer":"Sở Khoa học Công nghệ TP.HCM","description":"🎉 CHÚC MỪNG CÁC ĐỘI ĐẾN TỪ Câu lạc bộ AI - khoa Khoa học Máy tính UIT  ĐẠT NHIỀU GIẢI THƯỞNG XUẤT SẮC TẠI AI CHALLENGE 2024 🎉\n🏆 1 Giải Nhất (Team TycheVid - Quán quân hai năm liên tiếp 2023 & 2024)\nTrần Gia Bảo (K2022)\nBùi Công Khánh Tường (K2022)\nTrần Nhật Khoa (K2022)\nLê Thị Thanh Tâm (K2023)\nHồ Trọng Hiển (K2022)\n🥈 1 Giải Nhì (Team Stubborn_Strawberries)\nLê Bảo Long (K2023)\nNguyễn Tiến Thắng (K2023)\nNguyễn Thuận Phát (K2023)\nNguyễn Huy Phong (K2023)\nMinh Hoàng Thái (K2023)\n✨ 1 Giải Khuyến Khích (Team Rookie_UIT)\nNguyễn Viết Thiện Nhân (K2023)\nNguyễn Hùng Quân (K2023)\nPhan Thành Đạt (K2023)\nTrần Văn Hoàng (K2023)\nTrần Thúy An (K2023)\n🎊 Một lần nữa, xin chúc mừng các bạn đã ghi danh UIT tại AI Challenge 2024!","externalUrl":"","images":[{"full":"/api/uploads/2855e9b1fa0ef1078ff8.jpg","thumb":"/api/uploads/e3b7f55677e7303edfd8.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"giai-thuong-khoa-hoc-va-cong-nghe-danh-cho-sinh-vien-trong-co-so-giao-","competition":"Giải thưởng Khoa học và Công nghệ dành cho sinh viên trong cơ sở giáo dục đại học năm 2024","placement":"Giải Nhì","rank":2,"year":2024,"month":"11","scope":"national","domain":"","organizer":"Bộ Giáo dục & Đào tạo","description":"Chúc mừng nhóm sinh viên đến từ Câu lạc bộ AI - khoa Khoa học Máy tính UIT đạt được giải Nhì- Giải thưởng Khoa học và Công nghệ dành cho sinh viên trong cơ sở giáo dục đại học năm 2024 \nGiải thưởng khoa học và công nghệ dành cho sinh viên trong cơ sở giáo dục đại học do Bộ Giáo dục và Đào tạo (GDĐT), Bộ Khoa học và Công nghệ, Liên hiệp các Hội Khoa học và Kỹ thuật Việt Nam (Quỹ Hỗ trợ Sáng tạo Kỹ thuật Việt Nam - VIFOTEC), Trung ương Đoàn Thanh niên Cộng sản Hồ Chí Minh tổ chức hàng năm nhằm biểu dương thành tích xuất sắc và khuyến khích sinh viên tham gia nghiên cứu khoa học, góp phần nâng cao chất lượng đào tạo và bồi dưỡng tài năng khoa học trẻ trong các cơ sở giáo dục đại học. Tổng số đề tài tham gia giải thưởng năm 2024 là 536 đề tài của sinh viên 95 trường đại học.\nĐề tài mà nhóm sinh viên : Võ Anh Hào, Trần Siêu, Nguyễn Minh Đức hướng tới đề xuất giải pháp cho bài toán phát hiện không đội mũ bảo hiểm của người đi xe máy khi tham gia giao thông. Giải pháp đề xuất có thể tích hợp vào các hệ thống giao thông thông minh nhằm giám sát việc thực hiện quy định cũng như nâng cao ý thức người tham gia giao thông từ đó có thể góp phần giảm tỉ lệ tai nạn và tử vong.  \nThông tin chi tiết đề tài :\nTên đề tài \"GIẢI PHÁP TĂNG CƯỜNG ĐỘ CHÍNH XÁC  CHO HỆ THỐNG PHÁT HIỆN VI PHẠM ĐỘI MŨ BẢO HIỂM KHI THAM GIA GIAO THÔNG BẰNG CO-DETR VÀ ĐỀ XUẤT CÁC THUẬT TOÁN TỐI ƯU CHO LỚP THIỀU SỐ\"\nMột số kết quả đạt của đề tài bao gồm :\n- Đề xuất hai thuật toán, Minority Optimizer và Virtual Expander, nhằm cảithiện độ chính xác của giải pháp trên các lớp thiểu số.\n- Áp dụng mô hình CO-DETR cho việc phát hiện vi phạm quy định đội mũ bảo hiểm.\n- Công bố bài báo tại Workshop hội nghị CVPR 2024 \n- Giải pháp  đạt được Top 1 tại track 5 cuộc thi AI City Challenge 2024, được tổ chức bởi Nvidia trong khuôn khổ hội nghị CVPR 2024 với tống 726 đội thi đến từ 47 quốc gia trên thế giới.\nMột lần nữa chúc mừng các bạn","externalUrl":"","images":[{"full":"/api/uploads/149b32657abbe8a1fb6b.jpg","thumb":"/api/uploads/736822a308fff75f5f5c.jpg"},{"full":"/api/uploads/c857610965280dfb6a45.jpg","thumb":"/api/uploads/4e4d29d167bc272ac588.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]},{"id":"bkai-soict-hackathon-2024","competition":"BKAI – SoICT Hackathon","placement":"Giải Nhì","rank":2,"year":2024,"month":"","scope":"national","domain":"General AI","organizer":"HUST & BKAI","description":"Thành tích xuất sắc tại cuộc thi Hackathon chuyên sâu về AI.","externalUrl":"","images":[{"full":"/api/uploads/78443616364b55b80ad8.jpg","thumb":"/api/uploads/e47c4bd1b6219b91f58d.jpg"}],"featured":false,"memberSlugs":[],"projectSlugs":[]}],"projects":[{"summary":"Tìm một khoảnh khắc trong kho video hàng nghìn giờ, ngay tại chỗ. Phát triển cùng Viện Thông tin Quốc gia Nhật Bản (NII), mang đi thi VBS, TRECVID và AI Challenge TP.HCM.","problem":"Người dùng mô tả một cảnh bằng lời, hoặc đưa một ảnh mẫu, rồi cần tìm đúng đoạn video trong tập dữ liệu hơn 9TB trong vài phút. Truy vấn thường mơ hồ, thuộc nhiều ngôn ngữ, và có thể nhắm vào vật thể, khung cảnh, hành động hoặc một chi tiết rất nhỏ.","approach":"Truy vấn đa phương thức: nhúng ảnh–văn bản (CLIP, BEiT-3) kết hợp nhiều mô hình, tìm kiếm theo chuỗi thời gian cho các mô tả nhiều cảnh nối tiếp, dùng LLM để diễn giải truy vấn, và xếp hạng lại dựa trên ngữ cảnh quanh khung hình. Toàn bộ phải đủ nhanh để người dùng lặp truy vấn liên tục trong lúc thi.","outcome":"Best Overall System tại VBS 2025 (MMM 2025, Nhật Bản, hơn 17 đội quốc tế). Giải Nhất TRECVID 2024 (NIST). Tiếp tục dự VBS 2026 tại Praha. Chuỗi công bố liên tục từ VBS 2022 đến nay.","status":"competition","domains":["Video Understanding","Information Retrieval"],"stack":["CLIP","BEiT-3","LLM"],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":["nii-uit-at-vbs2026-towards-effective-visual-question-answering-for-int","scalable-context-aware-multimodal-interactive-video-retrieval","super-image-reranking-lightweight-temporal-context-aggregation-for-ad-","nii-uit-at-vbs2025-multimodal-video-retrieval-with-llm-integration-and","a-hybrid-video-retrieval-system-using-clip-and-beit-3-for-enhanced-und","unveiling-peripheral-information-a-context-aware-video-retrieval-appro","addressing-ambiguous-queries-in-video-retrieval-with-advanced-temporal","event-retrieval-from-large-video-collection-in-ho-chi-minh-city-ai-cha","diverse-search-methods-and-multi-modal-fusion-for-high-performance-vid","integrating-multiple-models-for-effective-video-retrieval-and-multi-st","uit-at-vbs-2022-an-unified-and-interactive-video-retrieval-system-with"],"awardIds":["video-browser-showdown-vbs-2025-2025","trecvid-nist-2024","hoi-thi-thu-thach-tri-tue-nhan-tao-tp-ho-chi-minh-nam-2025-2025","hoi-thi-thu-thach-tri-tue-nhan-tao-tp-ho-chi-minh-nam-2024-2024"],"slug":"truy-van-video-tuong-tac","title":"Hệ thống truy vấn video tương tác"},{"summary":"Trả lời câu hỏi về nội dung một video dài bằng cách truy hồi trước, trả lời sau. Bài dự thi TRECVID 2025 của đội NII-UIT.","problem":"Một video dài hàng giờ không nhét trọn được vào mô hình, trong khi câu hỏi thường chỉ liên quan tới vài đoạn ngắn. Phải tìm đúng những đoạn đó trước khi trả lời.","approach":"VRAG — truy hồi tăng cường cho hỏi đáp video: cắt và lập chỉ mục video, truy hồi các đoạn liên quan tới câu hỏi rồi mới sinh câu trả lời. Mã nguồn bản dự thi được công khai.","outcome":"Giải Nhất bài toán Video Question Answering (Multiple Choice) tại TRECVID 2025.","status":"competition","domains":["Video Understanding","NLP"],"stack":["Python"],"repository":"https://github.com/aiclub-uit/TrecVID2025_VQA","demo":"","images":[],"memberSlugs":[],"publicationIds":["vrag-retrieval-augmented-video-question-answering-for-long-form-videos","nii-uit-at-vbs2026-towards-effective-visual-question-answering-for-int"],"awardIds":["trecvid-2025-2025"],"slug":"hoi-dap-tren-video-dai","title":"Hỏi đáp trên video dài"},{"summary":"Phát hiện vi phạm và nhận dạng vật thể trên video camera giao thông. Bài dự thi AI City Challenge (CVPR / NVIDIA).","problem":"Camera giao thông quay liên tục ở độ phân giải thấp, xe máy che lấp nhau, còn ảnh mắt cá thì méo mạnh ở rìa khung. Phải nhận ra người không đội mũ bảo hiểm và định vị vật thể trên đường trong đúng những điều kiện đó.","approach":"","outcome":"Top 1 Track 5 — Detecting Violation of Helmet Rule for Motorcyclists, trên hơn 60 đội (AI City Challenge 2024, hội nghị CVPR 2024). Top 4 Track 4 — Road Object Detection in Fish-Eye Cameras, trên hơn 52 đội. Mã nguồn Track 5 công khai; hai repo cho mùa 2026 (Track 2, Track 4) đã mở trên GitHub của CLB.","status":"competition","domains":["Computer Vision"],"stack":["Python"],"repository":"https://github.com/aiclub-uit/AICITY2024_Track5","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":["ai-city-challenge-nvidia-cvpr-2024-2024"],"slug":"phan-tich-giao-thong-tu-camera","title":"Phân tích giao thông từ camera"},{"slug":"robot-tu-hanh","title":"Robot tự hành","summary":"Nhận diện đối tượng, xe tự hành.","problem":"Robot được thiết kế với khả năng nhận diện và tương tác với con người, ghi nhớ lộ trình và tránh chướng ngại vật trên đường đi. Các bài toán liên quan: định vị và dựng bản đồ đồng thời (SLAM), phát hiện vật thể, chuyển văn bản thành giọng nói, nhận dạng khuôn mặt.","approach":"","outcome":"","status":"active","domains":["Computer Vision","Robotics"],"stack":[],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]},{"slug":"rut-trich-thong-tin","title":"Rút trích thông tin","summary":"Rút trích thông tin từ hóa đơn, giấy tờ tùy thân, …","problem":"Xác định nội dung của các trường thông tin trong ảnh chụp văn bản, dùng cho việc số hóa dữ liệu và định danh khách hàng điện tử. Các bài toán liên quan: phát hiện chữ trong ảnh, nhận dạng chữ, rút trích thông tin, hỏi đáp trên văn bản.","approach":"","outcome":"","status":"active","domains":["Computer Vision"],"stack":[],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]},{"slug":"dem-luu-luong-xe","title":"Đếm lưu lượng xe","summary":"Đếm lưu lượng giao thông theo camera quan sát.","problem":"Xác định lưu lượng của mỗi loại phương tiện theo từng hướng đi, phục vụ theo dõi và thống kê giao thông trên phạm vi toàn thành phố. Các bài toán liên quan: phát hiện vật thể, theo vết nhiều đối tượng, thích nghi miền không giám sát, xử lý thời gian thực.","approach":"","outcome":"","status":"active","domains":["Computer Vision"],"stack":[],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]},{"slug":"nhan-dien-khuon-mat","title":"Nhận diện khuôn mặt","summary":"Hệ thống điểm danh sinh viên, mở cửa tự động.","problem":"Xác định người dùng qua ảnh chụp khuôn mặt, áp dụng để tự động hóa việc điểm danh hoặc trong các hệ thống giám sát. Các bài toán liên quan: phát hiện vật thể, phát hiện điểm mốc khuôn mặt, học biểu diễn, chống giả mạo.","approach":"","outcome":"","status":"active","domains":["Computer Vision"],"stack":[],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]},{"slug":"he-thong-quan-ly-bai-xe-thong-minh","title":"Hệ thống Quản lý Bãi xe Thông minh","summary":"Giải pháp tự động nhận diện biển số và quản lý dòng xe ra vào dựa trên Deep Learning.","problem":"","approach":"","outcome":"","status":"active","domains":["Computer Vision"],"stack":["Python","YOLOv8","FastAPI","React"],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]},{"slug":"tro-ly-ao-ho-tro-tuyen-sinh-dai-hoc","title":"Trợ lý Ảo hỗ trợ Tuyển sinh Đại học","summary":"Chatbot thông minh sử dụng RAG và LLM để cung cấp thông tin tuyển sinh chính xác cho thí sinh.","problem":"","approach":"","outcome":"","status":"active","domains":["NLP"],"stack":["Python","LangChain","OpenAI API","Vector Database"],"repository":"","demo":"","images":[],"memberSlugs":[],"publicationIds":[],"awardIds":[]}],"activities":[{"title":"CLB AI tại hội nghị MAPR 2026","slug":"clb-ai-tai-hoi-nghi-mapr-2026","date":"2026-08-15","category":"community","summary":"Thành viên CLB có mặt tại MAPR 2026 ở Huế.","content":"","url":"https://www.facebook.com/share/p/1DBHpnFz5Z/","location":"Huế","image":{"full":"/api/uploads/1a63f5789d5cd5c9577c.jpg","thumb":"/api/uploads/3816d9d17bf944ab8527.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Kết quả Tempo Run 2026","slug":"ket-qua-tempo-run-2026","date":"2026-08-08","category":"competition","summary":"Công bố kết quả chung cuộc Tempo Run 2026 — cuộc thi học thuật của CLB dành cho sinh viên năm nhất.","content":"🏆 KẾT QUẢ TEMPO RUN 2026 🏆\nSau hơn gần một tháng thi đấu sôi nổi, Tempo Run 2026 – cuộc thi học thuật dành cho sinh viên năm nhất – đã chính thức khép lại với nhiều thử thách và những màn bứt phá ấn tượng!\nBTC xin chúc mừng tất cả các thí sinh đã nỗ lực hết mình và cùng tạo nên một mùa giải đáng nhớ. Và đây là kết quả chung cuộc:\n🥇 GIẢI NHẤT: UIT.BTC\n🥈 GIẢI NHÌ: LongJustin\n🥉 GIẢI BA: PCTDN\n⭐️ GIẢI KHUYẾN KHÍCH: Tôi thăng cấp một mình\nBTC xin chúc mừng các đội đạt giải và cảm ơn tất cả các bạn sinh viên đã đồng hành cùng Tempo Run 2026. Thông tin về các bước tiếp theo sẽ được gửi qua email đến từng đội đạt giải.\nHãy tiếp tục giữ vững tinh thần học hỏi – sáng tạo – dám thử sức và cùng AIClub chinh phục những thử thách mới! 🚀💙\nEmail: aiclub@uit.edu.vn\nWebsite: https://aiclub.uit.edu.vn/ Ẩn bớt","url":"https://www.facebook.com/share/p/19LYKDbDqb/","location":"","image":{"full":"/api/uploads/7224f860ec14f7ec7a2d.jpg","thumb":"/api/uploads/9007f583fd42d73022da.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Tempo Run 2026 chính thức bắt đầu","slug":"tempo-run-2026-chinh-thuc-bat-dau","date":"2026-07-06","category":"competition","summary":"Vòng thi Tempo Run 2026 mở màn.","content":"🔥 TEMPO RUN 2026 CHÍNH THỨC BẮT ĐẦU! 🔥\nSau thời gian mở đăng ký, AI Club – UIT xin gửi lời cảm ơn đến tất cả các đội thi đã quan tâm và tham gia cuộc thi. Giờ là lúc các bạn bước vào thử thách chính thức: xây dựng hệ thống AI có khả năng truy xuất video và xác định chính xác khoảnh khắc được mô tả bằng ngôn ngữ tự nhiên.\n🎯 CHỦ ĐỀ TEMPO RUN 2026: TEMPORAL VIDEO RETRIEVAL\nTrong cuộc thi năm nay, các đội sẽ giải quyết bài toán xây dựng một hệ thống AI có khả năng:\n🔹 Tiếp nhận mô tả nội dung bằng ngôn ngữ tự nhiên.\n🔹 Tìm đúng video phù hợp trong kho dữ liệu.\n🔹 Xác định chính xác khung hình hoặc mốc thời gian xuất hiện nội dung được mô tả.\n🔹 Xếp hạng các kết quả dự đoán theo mức độ phù hợp.\nVí dụ, với mô tả:\n> “Một người đàn ông mặc áo khoác đỏ đi qua con đường phủ tuyết vào ban đêm.”\nHệ thống cần tìm được đúng video và đúng thời điểm diễn ra khoảnh khắc đó.\n📩 CÁC ĐỘI THI VUI LÒNG KIỂM TRA EMAIL\nBan Tổ chức đã gửi email hướng dẫn đến các đội đăng ký tham gia Tempo Run 2026. Các đội vui lòng kiểm tra hộp thư, bao gồm cả mục Spam, để không bỏ lỡ các thông tin quan trọng về:\n* Thể lệ và hướng dẫn tham gia cuộc thi.\n* Tài liệu hỗ trợ và baseline tham khảo.\n* Thông tin về dữ liệu, cách nộp bài và các mốc thời gian quan trọng.\n* Kênh liên lạc chính thức với Ban Tổ chức.\nTrong trường hợp đội thi chưa nhận được email hoặc gặp vấn đề trong quá trình truy cập thông tin, vui lòng liên hệ Ban Tổ chức qua kênh liên lạc bên dưới để được hỗ trợ kịp thời.\n📅 THỜI GIAN THI CHÍNH THỨC\n* Thời gian thi: 06/07/2026 - 26/07/2026\n🚀 Tempo Run 2026 không chỉ là một cuộc thi, mà còn là cơ hội để các bạn tiếp cận bài toán AI đa phương tiện thực tế, làm quen với các mô hình thị giác – ngôn ngữ như CLIP, SigLIP, video encoder, cũng như các kỹ thuật Cross-modal Retrieval và Temporal Video Grounding.\nChúc các đội thi có một hành trình thật bùng nổ, học được thật nhiều điều mới và tạo nên những kết quả ấn tượng tại Tempo Run 2026!\n—\n📧 Email: aiclub@uit.edu.vn\n📎 Website: https://aiclub.uit.edu.vn/\n🎯 Fanpage: https://facebook.com/uit.cs.aiclub\n🔔 Zalo: https://zalo.me/g/htufre4cbza18oyipuxd\n#TempoRun2026 #AIClubUIT #UIT #ArtificialIntelligence #VideoRetrieval #TemporalVideoRetrieval","url":"https://www.facebook.com/share/p/1DMQNj5eTB/","location":"","image":{"full":"/api/uploads/16253187a59554522191.jpg","thumb":"/api/uploads/ae9d16321e2e38da75eb.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Seminar giới thiệu và hướng dẫn tham gia HCM AI Challenge và Tempo Run","slug":"seminar-gioi-thieu-va-huong-dan-tham-gia-hcm-ai-challenge-va-tempo-run","date":"2026-06-29","category":"seminar","summary":"Buổi giới thiệu hai sân chơi và hướng dẫn cách tham gia cho sinh viên.","content":"🌈 SEMINAR GIỚI THIỆU & HƯỚNG DẪN THAM GIA HCM AI CHALLENGE và TEMPO RUN  🎉\nHCM AI Challenge 2026 là cuộc thi học thuật về Trí tuệ nhân tạo với chủ đề Truy vấn thông tin (Information Retrieval). Các đội thi sẽ xây dựng hệ thống có khả năng tìm kiếm, xử lý và đưa ra câu trả lời chính xác từ một kho dữ liệu lớn, đòi hỏi sự kết hợp của các kiến thức về NLP, Computer Vision và nhiều kỹ thuật AI hiện đại.\nNhằm hỗ trợ các bạn, đặc biệt là những bạn mới tìm hiểu về AI hoặc lần đầu tham gia các cuộc thi học thuật, CLB AI tổ chức buổi seminar chia sẻ về cuộc thi và những kiến thức nền tảng để bắt đầu xây dựng hệ thống!\n✨ Nội dung seminar:\n• Giới thiệu tổng quan về HCM AI Challenge 2026.\n• Phân tích đề bài và hướng tiếp cận.\n• Chia sẻ kinh nghiệm, tài liệu và giải đáp thắc mắc.\n📌 Thông tin chi tiết\n🗓️ Thời gian: 20:00 Thứ 6 - 03/07/2026\n📍 Địa điểm: https://meet.google.com/did-tsdo-rxp\n🌐 Website hội thi: https://aichallenge.hochiminhcity.gov.vn\nĐừng bỏ lỡ cơ hội tìm hiểu về cuộc thi và chuẩn bị hành trang để chinh phục HCM AI Challenge 2026! 🚀\n—\n📧 Email: aiclub@uit.edu.vn\n📎 Website: https://aiclub.uit.edu.vn/\n🎯 Fanpage: https://facebook.com/uit.cs.aiclub","url":"https://www.facebook.com/uit.cs.aiclub/posts/pfbid028rTu2azuKDxot5rCjUzS8DK47sGNX2gtBud1sgFHf9ViUmmBWfaFzSGojKx75BiEl","location":"","image":{"full":"/api/uploads/986e1889ccc2d54aee97.jpg","thumb":"/api/uploads/525eeb890577c9c586e8.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Tempo Run 2026 — Truy tìm khoảnh khắc","slug":"tempo-run-2026-truy-tim-khoanh-khac","date":"2026-06-27","category":"competition","summary":"Công bố chủ đề mùa 2026 của Tempo Run.","content":"🔥 TEMPO RUN 2026 – TRUY TÌM KHOẢNH KHẮC 🔥\n📣 Tiếp nối hành trình của Tempo Run 2025, AI Club – UIT chính thức mang đến Tempo Run 2026 với một thử thách hoàn toàn mới dành cho các bạn sinh viên Trường Đại học Công nghệ Thông tin – ĐHQG TP.HCM.\n🚀 Tempo Run là cuộc thi học thuật thường niên do CLB AI – UIT tổ chức, tạo cơ hội để sinh viên tiếp cận với trí tuệ nhân tạo thông qua một bài toán thực tế.\nBạn chưa có nhiều kiến thức về AI? Đừng lo! Ban Tổ chức sẽ cung cấp tài liệu hướng dẫn và baseline tham khảo, giúp bạn từng bước làm quen với xử lý dữ liệu đa phương tiện, mô hình thị giác – ngôn ngữ và các kỹ thuật truy xuất thông tin hiện đại.\n🎯 CHỦ ĐỀ TEMPO RUN 2026: TEMPORAL VIDEO RETRIEVAL\nNhiệm vụ của thí sinh là xây dựng một hệ thống AI có khả năng:\n🔹 Tiếp nhận mô tả nội dung bằng ngôn ngữ tự nhiên.\n🔹 Tìm đúng video phù hợp trong kho dữ liệu.\n🔹 Xác định chính xác khung hình hoặc mốc thời gian xuất hiện nội dung được mô tả.\n🔹 Xếp hạng các kết quả dự đoán theo mức độ phù hợp.\nVí dụ, với mô tả:\n> “Một người đàn ông mặc áo khoác đỏ đi qua con đường phủ tuyết vào ban đêm.”\nHệ thống cần tìm được đúng video và đúng thời điểm diễn ra khoảnh khắc đó.\n🌟 Đây là cơ hội để các bạn tiếp cận sớm với:\n- Bài toán xử lý dữ liệu video và văn bản.\n- Mô hình thị giác – ngôn ngữ như CLIP, SigLIP và video encoder.\n- Bài toán Cross-modal Retrieval và Temporal Video Grounding.\n- Quy trình xây dựng, kiểm thử và tối ưu một hệ thống AI hoàn chỉnh.\n🎓 LỢI ÍCH KHI THAM GIA TEMPO RUN 2026\nTham gia cuộc thi, các bạn sẽ nhận được:\n- Kiến thức, kiến thức và… thật nhiều kiến thức!\n- Cơ hội trải nghiệm một bài toán AI đa phương tiện thực tế.\n- Cơ hội giao lưu, học hỏi và làm việc cùng những người có chung niềm đam mê AI.\n🏆 CƠ CẤU GIẢI THƯỞNG DỰ KIẾN\n🥇 01 Giải Nhất\n🥈 01 Giải Nhì\n🥉 01 Giải Ba\n💡 01 Giải Ý tưởng sáng tạo\n📅 THỜI GIAN DIỄN RA\n- Hạn đăng ký: 20/07/2026\n- Thời gian thi chính thức: 06/07/2026 - 26/07/2026\n📝 Đăng ký tham gia: https://forms.gle/YPM7jxSrvKBh7UAM9\n📝 Sau khi đăng ký, các bạn vui lòng tham gia nhóm Zalo để cập nhật những thông báo mới nhất từ Ban Tổ chức: https://zalo.me/g/htufre4cbza18oyipuxd\nĐừng bỏ lỡ cơ hội khám phá thế giới AI, thử sức với dữ liệu video và truy tìm những khoảnh khắc tưởng chừng đang ẩn mình giữa hàng nghìn khung hình!\n🔥 Tempo Run 2026 – Truy tìm khoảnh khắc, kiến tạo hành trình AI!\n—\n📧 Email: aiclub@uit.edu.vn\n📎 Website: https://aiclub.uit.edu.vn/\n🎯 Fanpage: https://facebook.com/uit.cs.aiclub\n🔔 Zalo: https://zalo.me/g/htufre4cbza18oyipuxd\n#TempoRun2026 #AIClubUIT #UIT #ArtificialIntelligence #VideoRetrieval #TemporalVideoRetrieval Ẩn bớt","url":"https://www.facebook.com/share/p/19YRc27du8/","location":"","image":{"full":"/api/uploads/2ea370c2821ea2682762.jpg","thumb":"/api/uploads/46d877847e88fd324be5.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Bộ bài toán luyện tập chuẩn bị cho Olympic AI","slug":"bo-bai-toan-luyen-tap-chuan-bi-cho-olympic-ai","date":"2026-05-17","category":"competition","summary":"Hệ thống AI Challenge của UIT mở các bài toán luyện tập để các đội làm quen với format thi và những chủ đề đang được quan tâm trong AI hiện đại.","content":"🚀 CHUẨN BỊ CHO OLYMPIC AI SẮP TỚI!\nNhằm giúp các đội làm quen với format thi và thử sức với những chủ đề đang rất được quan tâm trong AI hiện đại, hệ thống AI Challenge chính thức mở bài toán mới: Adversarial Attack on VQA Models 🔥\nĐây là cơ hội để các đội tiếp cận một trong những hướng nghiên cứu nổi bật của AI Security và Computer Vision - nơi chỉ với những perturbation cực nhỏ gần như không thể nhận ra bằng mắt thường, mô hình AI vẫn có thể bị đánh lừa.\n💡 Trong bài toán lần này, các đội sẽ:\n• Tạo ảnh adversarial từ ảnh gốc\n• Tấn công mô hình VQA mục tiêu\n• Giữ chất lượng ảnh gần như không thay đổi\n• Tối ưu giữa khả năng attack và chỉ số PSNR\n🎯 Thử thách không chỉ yêu cầu mô hình dự đoán sai mà còn đòi hỏi perturbation đủ nhỏ để đảm bảo tính imperceptibility - một yếu tố quan trọng trong các nghiên cứu adversarial attack thực tế.\n🔍 Các hướng triển khai có thể thử nghiệm:\n• FGSM\n• PGD\n• Gradient-based attacks\n• Và nhiều kỹ thuật adversarial khác\n💪 Đây sẽ là dịp để các đội:\n• Tìm hiểu cách hoạt động và thử nghiệm các kỹ thuật Adversarial Attack trong Computer Vision và Multimodal AI.\n• Chuẩn bị cho các kỳ Olympic AI sắp tới\n👉 Tham gia ngay tại: https://aichallenge.uit.edu.vn","url":"https://aichallenge.uit.edu.vn/","location":"","image":{"full":"/api/uploads/e1222910ac4ea2492097.jpg","thumb":"/api/uploads/973ce47215407222fbac.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Sinh viên năm nhất UIT liên tiếp đạt Top 1 tại các cuộc thi quốc tế","slug":"sinh-vien-nam-nhat-uit-lien-tiep-dat-top-1-tai-cac-cuoc-thi-quoc-te","date":"2026-04-30","category":"competition","summary":"Chuỗi kết quả hạng nhất của các thành viên năm nhất trong nửa đầu 2026.","content":"🏆 SINH VIÊN NĂM NHẤT UIT LIÊN TIẾP ĐẠT TOP 1 TẠI CÁC CUỘC THI QUỐC TẾ 🌍\nCác bạn sinh viên năm nhất ngành Trí tuệ Nhân tạo – Khoa Khoa học Máy tính, UIT khi xuất sắc ghi dấu ấn tại các sân chơi học thuật quốc tế ngay từ năm đầu đại học!\n🎉 Tại hội nghị ICPR (International Conference on Pattern Recognition), bạn Phạm Duy Hiển đã xuất sắc đạt TOP 1 Track RGB (TVRID Challenge) – vượt qua 147 đội thi đến từ nhiều quốc gia. Cuộc thi tập trung vào bài toán định danh lại người từ góc nhìn trên cao (Top-View Person Re-Identification) – một hướng nghiên cứu khó trong thị giác máy tính, có tính ứng dụng cao trong giám sát thông minh và bảo vệ quyền riêng tư.\n🎉 Cũng trong thời gian này, đội sinh viên UIT đã tiếp tục mang về hạng Nhất tại ICDAR 2026 – CircleID Pen Classification, một trong những hội nghị hàng đầu thế giới về xử lý và hiểu tài liệu số, với sự tham gia của 389 đội thi quốc tế.\n👏 Xin chúc mừng các bạn:\n• Nguyễn Phạm Gia Huy\n• Ngô Viết Tuệ Đăng\n• Phạm Duy Hiển\n• Tạ Cao Nguyên Bảo\n💡 Cuộc thi tập trung vào bài toán phân loại nét bút và nhận dạng chữ viết tay – ứng dụng trong xác thực chữ ký, số hóa tài liệu và phát triển các hệ thống OCR trong điều kiện phức tạp.\n💙 Hai thành tích liên tiếp tại các hội nghị quốc tế không chỉ là niềm tự hào của cá nhân các bạn sinh viên, mà còn là minh chứng cho tinh thần chủ động học tập, dám thử thách và năng lực hội nhập mạnh mẽ của sinh viên UIT ngay từ những năm đầu tiên.\n✨ Từ giảng đường UIT đến các sân chơi học thuật toàn cầu – hành trình ấy đang được viết nên bởi chính các bạn sinh viên hôm nay.\n#VNUHCM #UIT\n#SinhVienUIT #AIUIT #ComputerScience\n#ICPR #ICDAR #GlobalAchievement\n#UIT20Nam #UIT20 #UIT20Years\n#SDGs #SDG4 #SDG9","url":"https://www.facebook.com/share/p/14n7mTsmomQ/","location":"","image":{"full":"/api/uploads/0c2b2ff8ba0d01ab79bb.jpg","thumb":"/api/uploads/fe1e0f6dde5839f88908.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["top-view-person-re-identification-icpr-2026-2026","icdar-2026-circleid-pen-classification-2026"]},{"title":"Top 1 tại ICPR 2026 — Track RGB (TVRID)","slug":"top-1-tai-icpr-2026-track-rgb-tvrid","date":"2026-04-17","category":"competition","summary":"Thành viên năm nhất ngành Trí tuệ Nhân tạo đạt hạng nhất ở cuộc thi thuộc khuôn khổ hội nghị ICPR.","content":"","url":"","location":"","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["top-view-person-re-identification-icpr-2026-2026"]},{"title":"Top 1 tại ICDAR 2026 — CircleID Pen Classification","slug":"top-1-tai-icdar-2026-circleid-pen-classification","date":"2026-04-15","category":"competition","summary":"Thành viên năm nhất ngành Trí tuệ Nhân tạo đạt hạng nhất tại cuộc thi thuộc khuôn khổ hội nghị ICDAR.","content":"","url":"","location":"","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["icdar-2026-circleid-pen-classification-2026"]},{"title":"UITAIC chính thức đi vào hoạt động","slug":"uitaic-chinh-thuc-di-vao-hoat-dong","date":"2026-03-28","category":"announcement","summary":"Hệ thống AI Challenge của Trường Đại học Công nghệ Thông tin bắt đầu vận hành.","content":"🚀 UITAIC chính thức đi vào hoạt động !\n🔥 UITAIC là hệ thống chấm bài tự động do AICLUB phát triển, được xây dựng với mục tiêu giúp sinh viên làm quen với các bài toán AI và từng bước tiếp cận, học tập các kỹ thuật căn bản trong lĩnh vực này.\n💡 Hệ thống hướng đến:\n- Luyện tập các bài toán trong lĩnh vực ML, CV, NLP\n- Làm quen với cách tiếp cận và giải quyết các bài toán AI trong thực tế\n- Xây dựng nền tảng kiến thức thông qua việc thực hành các bài toán thực tế\n🎯 Với mục tiêu:\n- Thúc đẩy tinh thần học tập chủ động\n- Khuyến khích luyện tập thường xuyên\n- Kết nối cộng đồng yêu thích lập trình & AI\n🚀 UITAIC không chỉ là một hệ thống chấm bài mà còn là bước khởi đầu giúp sinh viên UIT tiếp cận AI một cách bài bản và thực tế.\n👉 https://aichallenge.uit.edu.vn/","url":"https://aichallenge.uit.edu.vn/","location":"","image":{"full":"/api/uploads/c4da1919d7bda2466069.jpg","thumb":"/api/uploads/82eee7b995f6dd019cdd.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Top 5 tại ICPR 2026 — Low-Resolution License Plate Recognition","slug":"top-5-tai-icpr-2026-low-resolution-license-plate-recognition","date":"2026-03-04","category":"competition","summary":"Các thành viên năm nhất của CLB lọt vào top 5 chung cuộc.","content":"","url":"","location":"","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["icpr-2026-competition-on-low-resolution-license-plate-recognition-2026"]},{"title":"CLB AI tại Video Browser Showdown 2026","slug":"clb-ai-tai-video-browser-showdown-2026","date":"2026-01-31","category":"competition","summary":"Thành viên CLB tham gia VBS 2026 tại Praha, Cộng hoà Séc.","content":"Trải nghiệm thú vị của thành viên CLB tại cộng hoà Séc khi tham gia cuộc thi VBS 2026. Tuy không được kết quả cao nhưng dựa vào scoreboard  cho thấy system hoạt động tốt trên tất cả các dạng task. Trong đó system thể hiện tốt nhất dạng Text KIS nơi mà BTC chỉ cung cấp văn bản mô tả câu tìm kiếm mà không có hình ảnh mẫu, trong khi đó  dạng task mà có sự xuất hiện của video bị hụt hơi so với các team top trên. Nguyên nhân có thể có nhiều lý do trong đó BTC có yêu cầu không được capture màn hình để truy vấn, chỉ được chơi tối đa với số lượng theo số account được cung cấp bởi BTC- những việc mà Team CLB hoàn toàn tuân thủ theo quy định. \nĐây là chỉ là cuộc thi học thuật tuy nhiên việc tuân theo quy định thể hiện người có trách nhiệm, liêm chính- một trong những phẩm chất cần có đối với tất cả mọi người nói chung, đặc biệt là người muốn theo con đường học thuật lâu dài. \nĐặc biệt đây là cuộc thi quốc tế việc BTC có tất cả những bằng chứng cho thấy đội thi đấu không FAIR sẽ gây ấn tượng không tốt cho nhóm nghiên cứu, Trường, cộng đồng.","url":"https://www.facebook.com/share/p/18uvFfadQw/","location":"Praha, Cộng hoà Séc","image":{"full":"/api/uploads/0999fa4e6c0d68c45ffb.jpg","thumb":"/api/uploads/2fbf5ce6fc58a225cda0.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":["truy-van-video-tuong-tac"],"awardIds":[]},{"title":"Chào mừng các thành viên mới","slug":"chao-mung-cac-thanh-vien-moi","date":"2026-01-27","category":"community","summary":"CLB đón lứa thành viên mới.","content":"","url":"https://www.facebook.com/share/p/19hb1XWE9f/","location":"","image":{"full":"/api/uploads/991d26bcdbe5ef292f3d.jpg","thumb":"/api/uploads/8aad238912e447a6f9c7.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"slug":"clb-ai-vinh-du-duoc-don-prof-ngan-le-tu-dai-hoc-arkansas-ghe-tham","title":"CLB AI vinh dự được đón Prof. Ngân Lê từ Đại học Arkansas ghé thăm","date":"2025-12-18","category":"community","summary":"Chiều ngày 17/12 Khoa Khoa Khoa học Máy tính UIT và Câu lạc bộ AI - khoa Khoa học Máy tính UIT vinh dự được đón Prof. Ngân Lê từ Đại học Arkansas ghé thăm. Đã có nhiều trao đổi thú vị giữ Prof. và các bạn sinh viên, cũng","content":"Chiều ngày 17/12 Khoa Khoa Khoa học Máy tính UIT và Câu lạc bộ AI - khoa Khoa học Máy tính UIT vinh dự được đón Prof. Ngân Lê từ Đại học Arkansas ghé thăm. Đã có nhiều trao đổi thú vị giữ Prof. và các bạn sinh viên, cũng như hứa hẹn mở ra nhiều cơ hội hợp tác giữa phòng nghiên cứu của cô Ngân với các nhóm nghiên cứu của Khoa, Trường.","location":"","image":{"full":"/api/uploads/145262f39c7a59f20fea.jpg","thumb":"/api/uploads/c9ef90a437188777ca4e.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"Đội NightfoodRetrieval đạt Giải Ba Cuộc thi Thiết kế Điện tử Việt Nam 2025","slug":"doi-nightfoodretrieval-dat-giai-ba-cuoc-thi-thiet-ke-dien-tu-viet-nam-","date":"2025-12-15","category":"competition","summary":"Đội NightfoodRetrieval của UIT, với các thành viên đến từ CLB AI, đạt Giải Ba VEDC 2025.","content":"","url":"","location":"","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["thiet-ke-dien-tu-viet-nam-2025-2025"]},{"title":"CLB AI tại hội nghị SOICT 2025","slug":"clb-ai-tai-hoi-nghi-soict-2025","date":"2025-12-13","category":"community","summary":"Thành viên CLB có mặt tại SOICT 2025 ở Nha Trang.","content":"","url":"https://www.facebook.com/share/p/1GfKENXAVx/","location":"Nha Trang","image":{"full":"/api/uploads/5355bca1f27e6438f2ce.jpg","thumb":"/api/uploads/a0e332207ce8f48d9188.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]},{"title":"CLB AI tại vòng chung kết Viettel AI Race 2025","slug":"clb-ai-tai-vong-chung-ket-viettel-ai-race-2025","date":"2025-12-04","category":"community","summary":"Các thành viên CLB tại trụ sở Viettel, Hà Nội.","content":"","url":"","location":"Hà Nội","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["viettel-ai-race-2025-2025"]},{"title":"Các đội ĐHQG-HCM đoạt giải cao tại Hội thi AI Challenge TP.HCM 2025","slug":"cac-doi-dhqg-hcm-doat-giai-cao-tai-hoi-thi-ai-challenge-tp-hcm-2025","date":"2025-11-28","category":"competition","summary":"Trong đó có các đội với thành viên đến từ CLB AI.","content":"","url":"","location":"TP. Hồ Chí Minh","image":null,"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["hoi-thi-thu-thach-tri-tue-nhan-tao-tp-ho-chi-minh-nam-2025-2025"]},{"title":"Đội NII-UIT giành vị trí số 1 tại Video Browser Showdown 2025","slug":"nii-uit-vo-dich-video-browser-showdown-2025","date":"2025-01-09","category":"competition","summary":"NII-UIT — các thành viên CLB AI phối hợp cùng Viện Thông tin Quốc gia Nhật Bản (NII) — đứng đầu bảng xếp hạng chung cuộc VBS 2025.","content":"Video Browser Showdown lần thứ 14 diễn ra ngày 08/01/2025 tại hội nghị Multimedia Modeling (MMM) ở Nara, Nhật Bản, với 17 đội và 37 thí sinh tranh tài trên tập dữ liệu gần 4.000 giờ video.\n\nCuộc thi gồm hai phần: phần dành cho chuyên gia là chính người phát triển hệ thống, và phần dành cho người dùng mới. Đội NII-UIT xếp thứ nhất ở bảng tổng sắp.","url":"https://arxiv.org/abs/2509.12000","location":"Nara, Nhật Bản","image":{"full":"/api/uploads/3275c35206ca08940fe2.jpg","thumb":"/api/uploads/6cdf434fd3383c6a7fe1.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":["video-browser-showdown-vbs-2025-2025"]},{"title":"Cuộc thi Tempo Run 2024 — vòng Private Test","slug":"tempo-run-2024-private-test","date":"2024-11-24","category":"competition","summary":"Tempo Run là cuộc thi học thuật do CLB AI tổ chức cho sinh viên năm nhất UIT. Mùa 2024 lấy đề bài nhận diện bảng hiệu bằng trí tuệ nhân tạo.","content":"Cuộc thi mở cho cả những bạn chưa có nền tảng AI — ban tổ chức hướng dẫn từ đầu để thí sinh làm quen với xử lý dữ liệu, huấn luyện mô hình và thử sức với một bài toán thực tế.\n\nMùa 2024 diễn ra trong tháng 11: đăng ký từ 06/11, vòng Public Test từ 11/11 đến 22/11, sau đó là vòng Private Test — mỗi đội chọn mô hình tốt nhất của mình để dự đoán trên tập dữ liệu riêng, và kết quả vòng này quyết định thứ hạng chung cuộc. Các đội nộp kèm notebook để tranh giải sáng tạo.","url":"https://www.uit.edu.vn/clb-ai-chuc-cuoc-thi-tempo-run-2024-nhan-dien-bang-hieu-su-dung-tri-tue-nhan-tao","location":"","image":{"full":"/api/uploads/48e3e3040c751feb0b14.jpg","thumb":"/api/uploads/f8f2c43476ee88cb4c54.jpg"},"featured":false,"memberSlugs":[],"projectSlugs":[],"awardIds":[]}],"now":[],"resources":[{"id":"uitaic","group":"tool","title":{"vi":"UITAIC — Hệ thống AI Challenge của UIT","en":"UITAIC — UIT AI Challenge platform"},"desc":{"vi":"Nền tảng ra đề và chấm bài tự động cho các cuộc thi AI của trường, cũng là nơi CLB mở các bộ bài toán luyện tập.","en":"The university platform that hosts and auto-grades AI competition tasks, and where the club opens practice problem sets."},"url":"https://aichallenge.uit.edu.vn/","membersOnly":false}],"tracks":[{"id":"research","name":{"vi":"Nghiên cứu","en":"Research"},"doing":{"vi":"Đọc paper, chạy thí nghiệm, viết bài và nộp hội nghị cùng nhóm nghiên cứu.","en":"Read papers, run experiments, write and submit to conferences with a research group."},"learning":{"vi":"Phương pháp thí nghiệm, viết học thuật, đọc phản biện, quản lý kết quả.","en":"Experimental method, academic writing, critical reading, result management."},"leadsTo":{"vi":"Công bố tại hội nghị quốc tế, hồ sơ học bổng và cao học.","en":"Publications at international venues, a record for scholarships and grad school."}},{"id":"engineering","name":{"vi":"Kỹ thuật","en":"Engineering"},"doing":{"vi":"Xây hệ thống AI chạy thật: robot, pipeline dữ liệu, demo, hạ tầng, mã nguồn mở.","en":"Build AI systems that actually run: robots, data pipelines, demos, infrastructure, open source."},"learning":{"vi":"Thiết kế hệ thống, tối ưu mô hình, triển khai, làm việc trên repo chung.","en":"System design, model optimisation, deployment, working in a shared repository."},"leadsTo":{"vi":"Sản phẩm chạy được, portfolio kỹ thuật, cơ hội thực tập.","en":"Working products, an engineering portfolio, internship opportunities."}},{"id":"competition","name":{"vi":"Thi đấu","en":"Competition"},"doing":{"vi":"Vào đội tuyển, thử nghiệm nhanh, benchmark và nộp kết quả lên bảng xếp hạng.","en":"Join a team, iterate fast, benchmark, and submit to leaderboards."},"learning":{"vi":"Làm việc dưới deadline, đọc luật bài toán, phối hợp đội, kỷ luật thực nghiệm.","en":"Working to deadlines, reading task rules, team coordination, experimental discipline."},"leadsTo":{"vi":"Thành tích quốc gia và quốc tế, và thường là một bài báo đi kèm.","en":"National and international results — often with a paper attached."}},{"id":"learning","name":{"vi":"Học tập","en":"Learning"},"doing":{"vi":"Seminar hằng tuần, nhóm đọc, workshop và các buổi đào tạo cho sinh viên năm nhất.","en":"Weekly seminars, reading groups, workshops, and freshman training."},"learning":{"vi":"Nền tảng toán và học máy, kỹ năng trình bày, cách tự học một lĩnh vực mới.","en":"Maths and ML foundations, presenting, and how to learn a new field alone."},"leadsTo":{"vi":"Đủ nền để bước vào một trong ba hướng còn lại.","en":"Enough grounding to step into any of the other three tracks."}}],"journey":[{"id":"explore","label":{"vi":"Khám phá","en":"Explore"},"desc":{"vi":"Dự seminar, gặp các nhóm, xem CLB đang làm gì.","en":"Attend seminars, meet the groups, see what the club is working on."}},{"id":"learn","label":{"vi":"Học","en":"Learn"},"desc":{"vi":"Nhóm đọc và đào tạo nền tảng, có anh chị hướng dẫn.","en":"Reading groups and foundations training, with a mentor."}},{"id":"build","label":{"vi":"Xây","en":"Build"},"desc":{"vi":"Tham gia một dự án thật, viết code chạy được trên repo chung.","en":"Join a real project and ship code into a shared repository."}},{"id":"compete","label":{"vi":"Thi đấu / Nghiên cứu","en":"Compete / Research"},"desc":{"vi":"Vào đội tuyển hoặc nhóm nghiên cứu, hướng tới kết quả công bố.","en":"Join a competition team or a research group, aiming at a public result."}},{"id":"lead","label":{"vi":"Dẫn dắt","en":"Lead / Mentor"},"desc":{"vi":"Dẫn một nhóm, hướng dẫn khóa sau, giữ lại tri thức cho CLB.","en":"Lead a group, mentor the next cohort, keep the knowledge in the club."}}],"benefits":[{"id":"1","title":{"vi":"Chia sẻ kiến thức","en":""},"text":{"vi":"Tham gia CLB, bạn sẽ có cơ hội cùng nhau học những kiến thức mới, từ bạn bè, anh chị, Thầy Cô đi trước.","en":""},"image":null},{"id":"2","title":{"vi":"Cải thiện kỹ năng","en":""},"text":{"vi":"CLB có tổ chức các buổi seminar hàng tuần. Nội dung seminar trải rộng cả về kiến thức và các kỹ năng mềm như thiết kế slide, thuyết trình, phản biện.","en":""},"image":null},{"id":"3","title":{"vi":"Tài nguyên miễn phí","en":""},"text":{"vi":"Tại CLB AI, các bạn có cơ hội được sử dụng hệ thống máy chủ, Server có GPU của trường để phục vụ việc nghiên cứu thay vì giới hạn từ các công cụ công cộng.","en":""},"image":null},{"id":"4","title":{"vi":"Dự án thực tế","en":""},"text":{"vi":"Cơ hội tham gia các cuộc thi công nghệ và dự án thực tế cùng các chuyên gia trong CLB. Đây là lợi thế cạnh tranh cực lớn khi ứng tuyển việc làm.","en":""},"image":null}],"posts":[{"slug":"transformers","title":"🤖 Transformer: Giải Thích Chi Tiết Từ Bài Báo Gốc Đến BERT & GPT","date":"2026-05-04","topic":"paper","summary":"Năm 2017, 8 nhà nghiên cứu tại Google công bố bài báo với cái tên đầy tham vọng — “Attention Is All You Need”. Vào thời điểm ấy, ít ai ngờ rằng kiến trúc được giới thiệu trong bài báo đó — Transformer — sẽ trở thành nền móng cho gần như toàn bộ cuộc…","body":"> 📖 **Bài giảng toàn diện** về kiến trúc Transformer — từ động cơ ra đời, kiến trúc Encoder-Decoder, cơ chế Self-Attention, Positional Encoding, đến quá trình huấn luyện và kết quả thực nghiệm. Mở rộng sang GPT và BERT — hai “hậu duệ” đã thay đổi hoàn toàn ngành AI.\n\nNăm 2017, 8 nhà nghiên cứu tại Google công bố bài báo với cái tên đầy tham vọng — **“Attention Is All You Need”**. Vào thời điểm ấy, ít ai ngờ rằng kiến trúc được giới thiệu trong bài báo đó — **Transformer** — sẽ trở thành nền móng cho gần như toàn bộ cuộc cách mạng AI sau này: từ Google dịch, BERT trong cỗ máy tìm kiếm Google, cho tới GPT, Claude, Gemini và ChatGPT mà chúng ta dùng hằng ngày.\n\nBài viết này sẽ đi qua **toàn bộ nội dung của bài báo gốc** — từng công thức, từng con số, từng bảng ablation — rồi mở rộng sang hai “hậu duệ” nổi tiếng nhất: **GPT** (decoder-only) của OpenAI và **BERT** (encoder-only) của Google. Mục tiêu là khi đọc xong, bạn không chỉ *biết* Transformer là gì, mà còn *hiểu* vì sao scale bằng $d_k$, vì sao dùng sin-cos cho positional encoding, vì sao multi-head attention quan trọng, và vì sao BERT hiểu ngôn ngữ tốt hơn GPT nhưng GPT lại sinh văn bản giỏi hơn.\n\nBài viết được viết cho **người mới bắt đầu** — không yêu cầu kiến thức sâu về deep learning. Mọi khái niệm đều được giải thích từ trực giác, kèm ví dụ cụ thể và tính toán thủ công trước khi đưa ra công thức tổng quát.\n\n---\n\n# Mục lục\n\n1.  [Bối cảnh: Khi RNN chạm trần giới hạn](#1-boi-canh-khi-rnn-cham-tran-gioi-han)\n2.  [Transformer là gì? Từ “đọc tuần tự” sang “nhìn toàn cục”](#2-transformer-la-gi)\n3.  [Kiến trúc tổng thể: Encoder – Decoder](#3-kien-truc-tong-the-encoder-decoder)\n4.  [Tokenization và Embedding: Biến chữ thành số](#4-tokenization-va-embedding)\n5.  [Positional Encoding: Dạy Transformer biết thứ tự](#5-positional-encoding)\n6.  [Self-Attention: Trái tim của Transformer](#6-self-attention-trai-tim-cua-transformer)\n7.  [Multi-Head Attention: Nhìn từ nhiều góc độ](#7-multi-head-attention-nhin-tu-nhieu-goc-do)\n8.  [Feed-Forward Network, Residual Connection và Layer Normalization](#8-feed-forward-network-residual-va-layer-norm)\n9.  [Tại sao Self-Attention? So sánh với RNN và CNN](#9-tai-sao-self-attention)\n10.  [Huấn luyện Transformer](#10-huan-luyen-transformer)\n11.  [Kết quả thực nghiệm](#11-ket-qua-thuc-nghiem)\n12.  [Từ Transformer đến Pre-trained Language Models](#12-tu-transformer-den-pre-trained-language-models)\n13.  [GPT — Decoder-Only: Nghệ thuật sinh văn bản](#13-gpt-decoder-only)\n14.  [BERT — Encoder-Only: Bậc thầy hiểu ngôn ngữ](#14-bert-encoder-only)\n15.  [So sánh tổng hợp và dòng thời gian tiến hoá](#15-so-sanh-va-timeline)\n16.  [Kết luận](#16-ket-luan)\n17.  [Tài liệu tham khảo](#17-tai-lieu-tham-khao)\n\n---\n\n# 1\\. Bối cảnh: Khi RNN chạm trần giới hạn\n\nTrước năm 2017, gần như mọi bài toán xử lý chuỗi — dịch máy, tóm tắt văn bản, chatbot — đều dựa vào **RNN (Recurrent Neural Network)** và các biến thể cải tiến như **LSTM** (Long Short-Term Memory) và **GRU** (Gated Recurrent Unit).\n\n## Cách RNN hoạt động\n\nRNN xử lý dữ liệu theo **trình tự thời gian**: token sau chỉ được xử lý sau khi token trước hoàn thành. Tại mỗi bước thời gian *t*, mô hình tính:\n\n$h_t = f(h_{t-1}, x_t)$\n\nNghĩa là trạng thái ẩn $h_t$ phụ thuộc vào trạng thái trước đó $h_{t-1}$ và input hiện tại $x_t$. Thông tin được “truyền tay” từ đầu câu đến cuối câu qua chuỗi hidden state.\n\n![RNN vs LSTM vs GRU](/api/uploads/b970b93aa8933f1fb175.png)\n\n## Ba giới hạn cốt lõi của RNN\n\n**Thứ nhất: Không thể song song hoá.** Vì $h_t$ phải chờ $h_{t-1}$, nên dù bạn có hàng nghìn GPU, RNN vẫn phải xử lý từng token một. Với câu 100 từ, cần 100 bước tuần tự — không tận dụng được sức mạnh phần cứng hiện đại.\n\n**Thứ hai: Vanishing Gradient.** Khi chuỗi dài, gradient truyền ngược qua hàng trăm bước bị “tiêu biến” dần. LSTM và GRU giảm nhẹ vấn đề này bằng cơ chế gate, nhưng không giải quyết triệt để.\n\n**Thứ ba: Học kém quan hệ xa (long-range dependency).** Hãy xem câu:\n\n> *“The animal didn’t cross the street because **it** was too tired.”*\n\nĐể hiểu “it” ám chỉ “animal” (chứ không phải “street”), mô hình cần liên kết hai từ cách nhau 6 vị trí. Với RNN, thông tin về “animal” phải đi qua 6 hidden state trung gian — mỗi bước đều có nguy cơ bị suy hao. Câu càng dài, vấn đề càng nghiêm trọng.\n\n## Attention ban đầu: Giải pháp “vá lỗi” cho RNN\n\nNăm 2014, Bahdanau et al. đề xuất **cơ chế Attention** để cải thiện seq2seq: thay vì ép toàn bộ câu nguồn vào một vector duy nhất, decoder có thể “nhìn lại” từng hidden state của encoder và tập trung vào phần liên quan nhất.\n\nTuy nhiên, attention lúc này chỉ là **phụ kiện** cho RNN — backbone vẫn là recurrence tuần tự. Đến năm 2017, nhóm nghiên cứu tại Google đặt câu hỏi: *Nếu attention hiệu quả đến vậy, tại sao không dùng attention làm toàn bộ kiến trúc, bỏ hẳn recurrence?*\n\nVà Transformer ra đời.\n\n## So sánh trực quan RNN vs Transformer\n\nĐể hiểu rõ sự khác biệt, hãy xem cách hai kiến trúc xử lý câu “The cat sat on the mat”:\n\n**RNN:** Phải xử lý tuần tự 6 bước. Khi đến token “mat” (bước 6), thông tin về “The” (bước 1) đã đi qua 5 hidden state trung gian — mỗi bước đều có nguy cơ thông tin bị suy hao hoặc biến dạng. Trên GPU, 6 bước này **không thể chạy song song** vì bước sau phụ thuộc bước trước.\n\n**Transformer:** Xử lý cả 6 token **cùng lúc** trong một phép tính ma trận. Token “mat” trực tiếp “nhìn thấy” token “The” qua cơ chế attention — khoảng cách chỉ là **1 phép tính**, không phụ thuộc vào câu dài hay ngắn. Trên GPU, toàn bộ tính toán **hoàn toàn song song**.\n\nSự khác biệt này giải thích tại sao Transformer có thể train nhanh hơn RNN nhiều lần trên cùng phần cứng, đặc biệt với câu dài.\n\n---\n\n# 2\\. Transformer là gì?\n\nTransformer là kiến trúc neural network được giới thiệu trong bài báo **“Attention Is All You Need”** (Vaswani et al., 2017) tại hội nghị NeurIPS. Bài báo do 8 nhà nghiên cứu từ Google Brain, Google Research và University of Toronto đồng tác giả — và mỗi người đóng góp một mảnh ghép quan trọng:\n\n-   **Jakob Uszkoreit** đề xuất ý tưởng thay RNN bằng self-attention\n-   **Ashish Vaswani** và **Illia Polosukhin** thiết kế và implement Transformer đầu tiên\n-   **Noam Shazeer** đề xuất scaled dot-product attention và multi-head attention\n-   **Niki Parmar** thực nghiệm và tinh chỉnh vô số biến thể\n-   **Llion Jones** phụ trách codebase ban đầu và attention visualization\n-   **Łukasz Kaiser** và **Aidan Gomez** xây dựng tensor2tensor framework\n\nThông điệp cốt lõi nằm ngay trong tên bài báo:\n\n> *Nếu mô hình biết cách tập trung đúng chỗ (attention), thì recurrence không còn cần thiết.*\n\nTính đến nay, bài báo có hơn **130.000 lượt trích dẫn** — là một trong những bài báo khoa học được trích dẫn nhiều nhất mọi thời đại.\n\n## Tư duy khác biệt hoàn toàn\n\nRNN hỏi: *“Token này nên được xử lý sau token nào?”* — tức xử lý tuần tự.\n\nTransformer hỏi: *“Token này liên quan đến những token nào, và mức độ liên quan là bao nhiêu?”* — tức xử lý song song, nhìn toàn cục.\n\nCụ thể, Transformer:\n\n-   Nhìn **toàn bộ chuỗi cùng lúc** (không tuần tự)\n-   Mỗi token **tương tác trực tiếp** với mọi token khác (không cần trung gian)\n-   Mức độ tương tác được **học tự động** thông qua attention score\n-   Token đầu câu truy cập trực tiếp token cuối câu trong **một phép tính duy nhất**\n\nChỉ cần nhìn vào kết quả: Transformer đạt **28.4 BLEU** trên bài dịch Anh-Đức (WMT 2014), vượt mọi mô hình trước đó — kể cả ensemble — với chi phí huấn luyện chỉ bằng **một phần nhỏ**.\n\n---\n\n# 3\\. Kiến trúc tổng thể: Encoder – Decoder\n\nTrước khi đi vào từng thành phần, hãy nhìn bức tranh toàn cảnh.\n\n![Luồng dữ liệu toàn bộ Transformer — từ input đến output prediction](/api/uploads/a9024f915e789a6348fe.png)\n\n*Hình: Luồng dữ liệu toàn bộ Transformer — từ input đến output prediction*\n\n![Kiến trúc Transformer](/api/uploads/bdc0fd6444dd03385e53.png)\n\n*Hình: Stack 6 Encoder + 6 Decoder của Transformer gốc*\n\nTransformer gồm hai khối chính: **Encoder** (bộ mã hoá) và **Decoder** (bộ giải mã). Đây không phải ý tưởng mới — seq2seq đã có cấu trúc tương tự — nhưng Transformer thay thế hoàn toàn RNN bên trong bằng attention.\n\n## 3.1. Encoder — Bộ máy hiểu ngữ nghĩa\n\nEncoder nhận chuỗi input $(x_1, x_2, ..., x_n)$ và biến nó thành chuỗi biểu diễn liên tục $z = (z_1, z_2, ..., z_n)$. Mỗi $z_i$ là một vector chứa thông tin ngữ nghĩa của token $x_i$ **trong ngữ cảnh toàn bộ câu**.\n\nEncoder gồm **N = 6 layer giống hệt nhau** xếp chồng. Mỗi layer có 2 sub-layer:\n\n1.  **Multi-Head Self-Attention**: cho phép mỗi token “nhìn” tất cả token khác trong câu\n2.  **Position-wise Feed-Forward Network (FFN)**: mạng fully-connected áp dụng độc lập cho từng vị trí\n\nXung quanh mỗi sub-layer có **residual connection** (kết nối tắt) và **Layer Normalization**:\n\n$\\text{output} = \\text{LayerNorm}(x + \\text{Sublayer}(x))$\n\n![Encoder Block theo Figure 1 — “Attention Is All You Need”. Mũi tên “skip / residual” là residual connection.](/api/uploads/4c300156027b964c3ef6.png)\n\n*Hình: Encoder Block theo Figure 1 — “Attention Is All You Need”. Mũi tên “skip / residual” là residual connection.*\n\nTất cả sub-layer và embedding đều có chiều đầu ra **$d_{model} = 512$** để residual connection hoạt động được (vì phép cộng yêu cầu cùng kích thước).\n\n## 3.2. Decoder — Cỗ máy sinh chuỗi\n\nDecoder sinh chuỗi output $(y_1, y_2, ..., y_m)$ theo kiểu **auto-regressive**: tại mỗi bước, nó dùng các token đã sinh trước đó làm input để dự đoán token tiếp theo.\n\nDecoder cũng có **N = 6 layer**, nhưng mỗi layer có **3 sub-layer**:\n\n1.  **Masked Multi-Head Self-Attention**: giống encoder nhưng có **che (mask)** — token tại vị trí $i$ chỉ được nhìn các token $\\leq i$, không được “nhìn trước tương lai”\n2.  **Encoder-Decoder Attention (Cross-Attention)**: Query đến từ decoder, Key và Value đến từ output encoder — cho phép decoder tập trung vào phần input liên quan\n3.  **Feed-Forward Network**: giống encoder\n\n![Decoder Block theo Figure 1 — “Attention Is All You Need”. Điểm khác biệt: sub-layer 1 có causal mask (🔒), sub-layer 2 nhận K,V từ encoder (cross-attention).](/api/uploads/962f6ac215d545d3762f.png)\n\n*Hình: Decoder Block theo Figure 1 — “Attention Is All You Need”. Điểm khác biệt: sub-layer 1 có causal mask (🔒), sub-layer 2 nhận K,V từ encoder (cross-attention).*\n\n## 3.3. Ví dụ cụ thể: Dịch “I love AI” → “Tôi yêu AI”\n\nHãy theo dõi một câu đi qua Transformer:\n\n**Bước 1 — Encoder xử lý input:**\n\n-   Input: `[\"I\", \"love\", \"AI\"]`\n-   Mỗi token được embedding thành vector 512 chiều\n-   Cộng Positional Encoding để biết thứ tự\n-   Đi qua 6 layer encoder, mỗi layer có self-attention + FFN\n-   Kết quả: 3 vector $z_1, z_2, z_3$ — mỗi vector chứa thông tin ngữ nghĩa của token đó trong ngữ cảnh toàn câu\n\n**Bước 2 — Decoder sinh output từng token:**\n\n-   Bắt đầu với token đặc biệt `<start>`\n-   Decoder nhìn vào output encoder ($z_1, z_2, z_3$) qua cross-attention\n-   Dự đoán token đầu tiên: “Tôi”\n-   Lấy “Tôi” làm input mới, dự đoán tiếp: “yêu”\n-   Lấy \\[“Tôi”, “yêu”\\] làm input, dự đoán: “AI”\n-   Dự đoán `<end>` → dừng\n\n**Chi tiết hơn về luồng dữ liệu trong Decoder:**\n\nKhi decoder dự đoán token thứ 2 (“yêu”), nó thực hiện 3 phép attention theo thứ tự:\n\n1.  **Masked Self-Attention:** Input là \\[`<start>`, “Tôi”\\]. Token “Tôi” chỉ attend được `<start>` và chính nó (không nhìn token tương lai). Kết quả: vector ngữ cảnh của “Tôi” trong output đã sinh.\n    \n2.  **Cross-Attention:** Query đến từ kết quả bước 1. Key và Value đến từ output encoder ($z_1, z_2, z_3$ — tương ứng “I”, “love”, “AI”). Decoder “hỏi” encoder: “Để sinh token tiếp theo sau ‘Tôi’, tôi cần chú ý phần nào của câu input?” → Attention tập trung vào $z_2$ (“love”).\n    \n3.  **FFN:** Kết quả cross-attention đi qua FFN, đi qua linear + softmax → chọn token “yêu” từ vocabulary.\n    \n\nQuá trình lặp lại cho đến khi sinh token `<end>`.\n\n## 3.4. Ba họ mô hình từ Transformer\n\nSự linh hoạt của Transformer cho phép tách riêng hoặc kết hợp Encoder/Decoder:\n\n-   **Encoder-only** (chỉ dùng Encoder): phù hợp bài toán **hiểu** — phân loại, trích xuất thông tin. Ví dụ: **BERT**\n-   **Decoder-only** (chỉ dùng Decoder): phù hợp bài toán **sinh** — viết văn bản, chatbot. Ví dụ: **GPT**\n-   **Encoder-Decoder đầy đủ**: phù hợp bài toán **chuyển đổi chuỗi** — dịch máy, tóm tắt. Ví dụ: **T5**, **Transformer gốc**\n\n---\n\n# 4\\. Tokenization và Embedding\n\nTrước khi Transformer xử lý được gì, văn bản phải được chuyển thành **số**. Quá trình này gồm hai bước.\n\n## 4.1. Tokenization — Tách văn bản thành đơn vị xử lý\n\nMáy tính không hiểu “từ” — nó chỉ xử lý chuỗi ký hiệu rời rạc. Tokenization chia văn bản thành các **token** mà mô hình có thể xử lý.\n\nBài báo gốc dùng hai phương pháp:\n\n-   **Byte-Pair Encoding (BPE)** cho tiếng Đức: shared vocab ~37.000 token\n-   **WordPiece** cho tiếng Pháp: vocab 32.000 token\n\nÝ tưởng chung: thay vì mỗi từ là một token (dẫn đến vocab khổng lồ), ta tách từ thành **các đơn vị nhỏ hơn** dựa trên tần suất xuất hiện.\n\n**Ví dụ với BPE:**\n\n```\n\"Transformer is powerful\"\n→ [\"Transform\", \"er\", \" is\", \" power\", \"ful\"]\n```\n\nTừ hiếm “Transformer” bị tách thành “Transform” + “er” (cả hai đều phổ biến), giúp giảm kích thước vocabulary đáng kể và xử lý tốt từ mới.\n\n## 4.2. Embedding — Ánh xạ token thành vector\n\nSau tokenization, mỗi token chỉ là một ID số nguyên (ví dụ: “love” → ID 4523). Embedding biến ID này thành **vector liên tục** trong không gian $d_{model}$ chiều.\n\n**Ví dụ cụ thể** với $d_{model} = 512$:\n\n```\n\"I\"    → ID 42   → [0.9, 0.1, -0.4, ..., 0.3]    (512 số)\n\"love\"  → ID 4523 → [0.3, 1.2,  0.5, ..., -0.1]   (512 số)\n\"AI\"    → ID 891  → [-0.7, 0.8,  1.1, ..., 0.6]   (512 số)\n```\n\nEmbedding layer thực chất là một **bảng tra (lookup table)** kích thước $|V| \\times d_{model}$ ($|V|$ là kích thước vocabulary), được **học cùng mô hình** qua backpropagation. Sau huấn luyện, các từ có nghĩa gần nhau sẽ có vector gần nhau trong không gian embedding.\n\n**Một chi tiết quan trọng:** trong Transformer gốc, cùng một ma trận embedding được **chia sẻ** cho cả input embedding, output embedding, và lớp linear trước softmax cuối cùng. Tại lớp embedding, trọng số được nhân thêm $d_{model}$ để cân bằng scale với positional encoding.\n\n**Tại sao nhân $d_{model}$?** Positional encoding có giá trị trong khoảng \\[-1, 1\\] (sin/cos). Nếu embedding cũng có giá trị nhỏ (do random init hoặc normalizing), thì PE sẽ “lấn át” embedding — mô hình nhớ vị trí nhưng quên nghĩa. Nhân $\\sqrt{512} \\approx 22.6$ giúp embedding có scale lớn hơn, cân bằng với PE khi cộng lại.\n\n## 4.3. Tổng hợp: Từ text → vector sẵn sàng cho Transformer\n\nHãy theo dõi câu “I love AI” qua toàn bộ quá trình:\n\n```\nBước 1 - Tokenization (BPE):\n  \"I love AI\" → [\"I\", \"love\", \"AI\"]  (3 token)\n\nBước 2 - Token ID:\n  \"I\" → 42,  \"love\" → 4523,  \"AI\" → 891\n\nBước 3 - Embedding lookup (bảng 37000 × 512):\n  42   → x₁ = [0.9, 0.1, -0.4, ..., 0.3]     (512 số)\n  4523 → x₂ = [0.3, 1.2,  0.5, ..., -0.1]     (512 số)\n  891  → x₃ = [-0.7, 0.8, 1.1, ..., 0.6]      (512 số)\n\nBước 4 - Nhân √d_model:\n  x₁ = x₁ × √512 ≈ x₁ × 22.6\n\nBước 5 - Cộng Positional Encoding:\n  z₁ = x₁ + PE₀    (biết \"I\" ở vị trí 0)\n  z₂ = x₂ + PE₁    (biết \"love\" ở vị trí 1)\n  z₃ = x₃ + PE₂    (biết \"AI\" ở vị trí 2)\n\n→ z₁, z₂, z₃ sẵn sàng đưa vào Encoder!\n```\n\n---\n\n# 5\\. Positional Encoding\n\n## 5.1. Vấn đề: Transformer không biết thứ tự\n\nĐây là điểm quan trọng nhất cần hiểu: **Transformer hoàn toàn không có khái niệm thứ tự một cách tự nhiên.**\n\nKhác với RNN (xử lý token lần lượt nên tự biết thứ tự), Transformer xử lý toàn bộ chuỗi **song song**. Nếu không bổ sung thông tin vị trí, hai câu sau hoàn toàn giống nhau trong mắt Transformer:\n\n> “Tôi ăn cơm” và “Cơm ăn tôi”\n\nCả hai chỉ là cùng một **tập** token `{\"Tôi\", \"ăn\", \"cơm\"}` — thứ tự không tồn tại.\n\n## 5.2. Giải pháp: Cộng vector vị trí vào embedding\n\nÝ tưởng rất đơn giản: tạo một vector vị trí $PE_{pos}$ cho mỗi vị trí $pos$ trong câu, rồi **cộng trực tiếp** vào token embedding:\n\n$z_i = x_i + PE_i$\n\n-   $x_i$: embedding ngữ nghĩa của token (biết token đó **là gì**)\n-   $PE_i$: encoding vị trí (biết token đó **ở đâu**)\n-   $z_i$: embedding cuối cùng đưa vào encoder/decoder (biết cả **là gì** và **ở đâu**)\n\n## 5.3. Công thức sin-cos\n\nBài báo gốc dùng hàm sin và cos với các tần số khác nhau:\n\n$PE_{(pos, 2i)} = \\sin\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)$\n\n$PE_{(pos, 2i+1)} = \\cos\\left(\\frac{pos}{10000^{2i/d_{model}}}\\right)$\n\nTrong đó:\n\n-   $pos$: vị trí token trong câu (0, 1, 2, …)\n-   $i$: chỉ số chiều trong vector (0, 1, 2, …, $d_{model}/2 - 1$)\n-   Chiều **chẵn** dùng sin, chiều **lẻ** dùng cos\n\n## 5.4. Ví dụ cụ thể\n\nVới câu “I love AI” ($d_{model} = 4$ để dễ minh hoạ):\n\n**Token “I” ở vị trí pos=0:**\n\n```\ndim 0: sin(0 / 10000^(0/4)) = sin(0)     = 0.000\ndim 1: cos(0 / 10000^(0/4)) = cos(0)     = 1.000\ndim 2: sin(0 / 10000^(2/4)) = sin(0)     = 0.000\ndim 3: cos(0 / 10000^(2/4)) = cos(0)     = 1.000\n→ PE₀ = [0.000, 1.000, 0.000, 1.000]\n```\n\n**Token “love” ở vị trí pos=1:**\n\n```\ndim 0: sin(1 / 10000^(0/4)) = sin(1)     = 0.841\ndim 1: cos(1 / 10000^(0/4)) = cos(1)     = 0.540\ndim 2: sin(1 / 10000^(2/4)) = sin(0.01)  = 0.010\ndim 3: cos(1 / 10000^(2/4)) = cos(0.01)  = 0.999\n→ PE₁ = [0.841, 0.540, 0.010, 0.999]\n```\n\n**Quan sát quan trọng:**\n\n-   **Dim 0** (tần số cao): thay đổi nhanh giữa các vị trí liền kề → phân biệt các token gần nhau\n-   **Dim 2** (tần số thấp): thay đổi chậm → phân biệt các token ở xa nhau\n-   Kết hợp nhiều tần số → mỗi vị trí có **“dấu vân tay” duy nhất**\n\n![Positional Encoding Heatmap](/api/uploads/ff0abbce5538575bf104.png)\n\n*Hình: Heatmap Positional Encoding cho 20 token × 512 chiều. Mỗi hàng là một vị trí, mỗi cột là một chiều. Các chiều đầu thay đổi nhanh, các chiều sau thay đổi chậm.*\n\n## 5.5. Tại sao dùng sin-cos mà không học embedding vị trí?\n\nBa lý do chính:\n\n**Lý do 1 — Tổng quát hoá cho chuỗi dài hơn:** Sin-cos cho phép mô hình suy luận trên chuỗi **dài hơn** những gì nó gặp trong training, vì hàm sin-cos xác định cho mọi $pos$. Learned embedding bị giới hạn bởi số vị trí đã học.\n\n**Lý do 2 — Học được vị trí tương đối:** Nhờ tính chất lượng giác:\n\n$\\sin(\\omega \\cdot (pos+k)) = \\sin(\\omega \\cdot pos) \\cdot \\cos(\\omega \\cdot k) + \\cos(\\omega \\cdot pos) \\cdot \\sin(\\omega \\cdot k)$\n\n$PE_{pos+k}$ có thể biểu diễn như **phép biến đổi tuyến tính** (phép xoay 2D) của $PE_{pos}$. Điều này giúp mô hình dễ dàng học “token cách tôi 3 vị trí” — tức **vị trí tương đối** — chỉ bằng một phép nhân ma trận.\n\n**Lý do 3 — Không tốn thêm tham số:** Positional encoding được tính theo công thức cố định, không thêm parameter nào vào mô hình.\n\nBài báo cũng thử nghiệm learned positional embedding và cho kết quả **gần như giống hệt** (BLEU 25.7 vs 25.8). Tuy nhiên, tác giả chọn sinusoidal vì khả năng extrapolation.\n\n## 5.6. Các biến thể Positional Encoding hiện đại\n\nNgoài sin-cos gốc, các mô hình sau này phát triển nhiều biến thể:\n\n-   **Learned Positional Embedding** (BERT, GPT-1): mỗi vị trí có vector embedding học được. Đơn giản nhưng bị giới hạn bởi max sequence length khi train.\n-   **Relative Positional Encoding** (Transformer-XL, T5): thay vì encode vị trí tuyệt đối, encode khoảng cách tương đối giữa hai token. Ưu điểm: tốt hơn cho chuỗi dài.\n-   **Rotary Positional Embedding — RoPE** (LLaMA, GPT-NeoX): xoay vector Q/K thay vì cộng PE vào embedding. Kết hợp ưu điểm của cả absolute và relative, hiện là lựa chọn phổ biến nhất cho LLM.\n\n---\n\n# 6\\. Self-Attention: Trái tim của Transformer\n\nNếu phải chọn **một thành phần duy nhất** làm nên Transformer, đó chính là **Self-Attention** — hay chính xác hơn là **Scaled Dot-Product Attention**.\n\n## 6.1. Ý tưởng cốt lõi\n\nKhi xử lý một token, Transformer không đọc tuần tự. Nó hỏi:\n\n> *“Để hiểu token này, tôi cần chú ý đến token nào khác, và chú ý bao nhiêu?”*\n\nQuay lại ví dụ:\n\n> *“The animal didn’t cross the street because **it** was too tired.”*\n\nKhi xử lý token “it”, self-attention tính **điểm liên quan** giữa “it” với mọi token khác trong câu. Kết quả: “animal” nhận trọng số cao nhất → mô hình hiểu “it” ám chỉ “animal”.\n\n![Self-attention visualization](/api/uploads/5f85f00a27e48125cd94.png)\n\n*Hình: Self-attention khi xử lý “it” — mô hình tập trung mạnh vào “The” và “animal”*\n\n## 6.2. Query, Key, Value — Ba vai trò của mỗi token\n\nĐây là khái niệm then chốt. Mỗi token embedding $x$ được nhân với 3 ma trận trọng số khác nhau để tạo ra 3 vector:\n\n$Q = x \\cdot W^Q \\quad K = x \\cdot W^K \\quad V = x \\cdot W^V$\n\n**Cách hiểu trực quan qua phép ẩn dụ thư viện:**\n\nHãy tưởng tượng bạn vào thư viện tìm sách:\n\n-   **Query (Q)** — Câu hỏi bạn đang tìm: *“Tôi muốn tìm sách về machine learning”*\n-   **Key (K)** — Nhãn/tiêu đề trên gáy sách: *“Deep Learning”, “Cooking 101”, “Machine Learning Basics”*\n-   **Value (V)** — Nội dung thực sự bên trong cuốn sách\n\nBạn so khớp Query với từng Key (tính điểm tương đồng), cuốn nào “khớp” nhất thì bạn đọc Value của nó nhiều nhất.\n\n**Trong self-attention:** mỗi token đồng thời đóng cả 3 vai — vừa “hỏi” (Query), vừa “quảng cáo bản thân” (Key), vừa “cung cấp thông tin” (Value).\n\n**Cách hiểu thêm qua ví dụ cụ thể:**\n\nXét câu: *“Mèo ngồi trên thảm”*\n\nKhi xử lý token “ngồi”:\n\n-   **Query của “ngồi”** hỏi: *“Ai ngồi? Ngồi ở đâu?”*\n-   **Key của “Mèo”** trả lời: *“Tôi là chủ ngữ — sinh vật có thể ngồi”* → score cao\n-   **Key của “thảm”** trả lời: *“Tôi là địa điểm”* → score vừa\n-   **Key của “trên”** trả lời: *“Tôi là giới từ chỉ vị trí”* → score thấp hơn\n\nSau softmax, attention weights có thể là: Mèo=0.45, ngồi=0.10, trên=0.15, thảm=0.30.\n\nOutput cho “ngồi” = 0.45×Value(Mèo) + 0.10×Value(ngồi) + 0.15×Value(trên) + 0.30×Value(thảm)\n\nNghĩa là: vector mới của “ngồi” đã **hấp thụ** thông tin ngữ cảnh — biết rằng chủ ngữ là “Mèo” và địa điểm là “thảm”.\n\n## 6.3. Công thức Scaled Dot-Product Attention\n\n$\\text{Attention}(Q, K, V) = \\text{softmax}\\left(\\frac{QK^T}{\\sqrt{d_k}}\\right)V$\n\n![6.3. Công thức Scaled Dot-Product Attention](/api/uploads/1593b38db7183572e386.png)\n\n## 6.4. Ví dụ tính thủ công từng bước\n\nĐể thực sự hiểu, hãy tính tay với câu “I love AI” ($d_k = 3$ để đơn giản):\n\n**Bước 1 — Tạo Q, K, V:**\n\nGiả sử sau khi nhân ma trận, ta có:\n\n| Token | Q | K | V |\n| --- | --- | --- | --- |\n| I | \\[1, 0, 1\\] | \\[1, 1, 0\\] | \\[0.5, 0.3, 0.8\\] |\n| love | \\[0, 1, 0\\] | \\[0, 1, 1\\] | \\[0.2, 0.9, 0.1\\] |\n| AI | \\[1, 1, 0\\] | \\[1, 0, 1\\] | \\[0.7, 0.5, 0.6\\] |\n\n**Bước 2 — Tính điểm attention cho token “I” (Query₁ vs tất cả Key):**\n\n```\nscore(I, I)    = Q₁ · K₁ = 1×1 + 0×1 + 1×0 = 1\nscore(I, love) = Q₁ · K₂ = 1×0 + 0×1 + 1×1 = 1\nscore(I, AI)   = Q₁ · K₃ = 1×1 + 0×0 + 1×1 = 2\n```\n\n**Bước 3 — Scale bằng $\\sqrt{d_k} = \\sqrt{3} \\approx 1.73$:**\n\n```\nscaled scores = [1/1.73, 1/1.73, 2/1.73] = [0.58, 0.58, 1.15]\n```\n\n**Bước 4 — Softmax (chuẩn hoá thành xác suất):**\n\n```\nsoftmax([0.58, 0.58, 1.15]) ≈ [0.25, 0.25, 0.50]\n```\n\nToken “AI” nhận trọng số cao nhất (0.50) khi xử lý token “I”.\n\n**Bước 5 — Tổng trọng số các Value:**\n\n```\noutput₁ = 0.25 × V₁ + 0.25 × V₂ + 0.50 × V₃\n        = 0.25 × [0.5, 0.3, 0.8] + 0.25 × [0.2, 0.9, 0.1] + 0.50 × [0.7, 0.5, 0.6]\n        = [0.525, 0.550, 0.525]\n```\n\nKết quả là một vector mới cho token “I” — đã “hấp thụ” thông tin từ cả “love” và “AI”, với “AI” đóng góp nhiều nhất.\n\n## 6.5. Tại sao phải chia cho $d_k$?\n\nĐây là câu hỏi quan trọng mà bài báo giải thích rất rõ.\n\nGiả sử các thành phần của $q$ và $k$ là biến ngẫu nhiên độc lập, trung bình 0, phương sai 1. Tích vô hướng:\n\n$q \\cdot k = \\sum_{i=1}^{d_k} q_i k_i$\n\ncó trung bình bằng 0 nhưng **phương sai bằng $d_k$**. Khi $d_k$ lớn (thường là 64), các giá trị $q \\cdot k$ có thể rất lớn hoặc rất nhỏ. Khi đưa vào softmax:\n\n-   Giá trị lớn → softmax gần **1** (gần như chỉ chú ý 1 token)\n-   Giá trị nhỏ → softmax gần **0**\n-   Gradient ở vùng bão hoà → **gần bằng 0** → mô hình không học được\n\nChia cho $d_k$ đưa phương sai về 1, giữ softmax ở vùng có gradient tốt.\n\n## 6.6. Masked Self-Attention trong Decoder\n\nKhi huấn luyện, decoder nhận toàn bộ chuỗi target cùng lúc (để song song hoá). Nhưng nếu không mask, token ở vị trí $t = 2$ sẽ “nhìn thấy” token ở $t = 3$ — tức **gian lận** vì biết trước câu trả lời.\n\nGiải pháp: set attention score ở vị trí $j > i$ bằng $-\\infty$ trước khi softmax → softmax cho trọng số 0 → token chỉ nhìn được các token phía trước.\n\nMa trận mask cho 4 token:\n\n```\n         Tôi   yêu   câu   lạc\nTôi    [ 0    -∞    -∞    -∞  ]\nyêu    [ 0     0    -∞    -∞  ]\ncâu    [ 0     0     0    -∞  ]\nlạc    [ 0     0     0     0  ]\n```\n\n---\n\n# 7\\. Multi-Head Attention: Nhìn từ nhiều góc độ\n\n## 7.1. Vấn đề của Single-Head Attention\n\nMột attention head duy nhất chỉ tính **một kiểu** trọng số — nó phải nén tất cả các kiểu quan hệ (ngữ pháp, ngữ nghĩa, vị trí, coreference…) vào cùng một bộ trọng số. Bài báo chỉ ra: single-head kém hơn best setting **0.9 BLEU**.\n\n## 7.2. Giải pháp: chạy song song nhiều head\n\nThay vì 1 attention với $d_{model}$ chiều, ta dùng $h$ head, mỗi head làm attention trên không gian con $d_k = d_{model}/h$ chiều:\n\n$\\text{MultiHead}(Q, K, V) = \\text{Concat}(\\text{head}_1, ..., \\text{head}_h) \\cdot W^O$\n\n$\\text{trong đó}\\quad\\text{head}_i = \\text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$\n\nTrong Transformer gốc: **h = 8** head, $d_k = d_v = 512/8 = 64$.\n\n## 7.3. Ví dụ trực quan\n\nVới câu “I love AI club” → “Tôi yêu câu lạc bộ AI”:\n\n-   **Head 1 (syntactic):** “câu lạc bộ” chú ý mạnh vào “club” — track cấu trúc ngữ pháp\n-   **Head 2 (semantic):** “yêu” liên kết chặt với “love” — ánh xạ ngữ nghĩa\n-   **Head 3 (positional):** mỗi token chú ý token liền kề — nắm thứ tự cục bộ\n-   **Head 4-8:** học các pattern khác\n\nKhông head đơn lẻ nào làm được tất cả. Multi-head chạy song song 8 “góc nhìn”, rồi nối (concat) kết quả thành vector 512 chiều, nhân với $W^O$ để tổng hợp.\n\n## 7.4. Chi tiết toán học\n\n**Bước 1 — Linear Projection:** Input Q, K, V (mỗi cái kích thước $n \\times 512$) được nhân với 8 bộ ma trận khác nhau:\n\n```\nHead 1: Q₁ = Q·W₁ᵠ (n×64),  K₁ = K·W₁ᴷ (n×64),  V₁ = V·W₁ⱽ (n×64)\nHead 2: Q₂ = Q·W₂ᵠ (n×64),  K₂ = K·W₂ᴷ (n×64),  V₂ = V·W₂ⱽ (n×64)\n...\nHead 8: Q₈ = Q·W₈ᵠ (n×64),  K₈ = K·W₈ᴷ (n×64),  V₈ = V·W₈ⱽ (n×64)\n```\n\nMỗi head làm việc trong **không gian con 64 chiều** — nhỏ hơn nhưng chuyên biệt hơn.\n\n**Bước 2 — Attention độc lập:** Mỗi head tính Scaled Dot-Product Attention riêng → 8 output, mỗi cái kích thước $n \\times 64$.\n\n**Bước 3 — Concat:** Nối 8 output lại: $n \\times (8 \\times 64) = n \\times 512$.\n\n**Bước 4 — Linear cuối:** Nhân với $W^O$ ($512 \\times 512$) để trộn thông tin từ 8 head → output cuối $n \\times 512$.\n\nĐiểm hay: tổng chi phí tính toán **gần bằng** single-head attention với full 512 chiều (vì $8 \\times 64 = 512$), nhưng mô hình học được **nhiều kiểu quan hệ** hơn.\n\n## 7.5. Ba nơi dùng attention trong Transformer\n\nTransformer dùng cùng cơ chế Multi-Head Attention nhưng ở **3 vị trí khác nhau** với input Q/K/V khác nhau:\n\n**1\\. Encoder Self-Attention:**\n\n```\nQ = output layer encoder trước\nK = output layer encoder trước  (cùng nguồn với Q)\nV = output layer encoder trước  (cùng nguồn với Q)\n```\n\nMỗi token input **nhìn tất cả** token input khác → biểu diễn ngữ cảnh hoàn toàn bidirectional. Không có mask.\n\nVí dụ: Khi encode “sat” trong “The cat sat on the mat”, self-attention cho phép “sat” biết chủ ngữ là “cat” và bổ ngữ là “on the mat”.\n\n**2\\. Decoder Masked Self-Attention:**\n\n```\nQ = output layer decoder trước\nK = output layer decoder trước  (cùng nguồn)\nV = output layer decoder trước  (cùng nguồn)\n+ MASK: chỉ nhìn vị trí ≤ hiện tại\n```\n\nGiống encoder self-attention nhưng có **causal mask** — token chỉ attend phía trước. Đảm bảo tính auto-regressive.\n\n**3\\. Encoder-Decoder Attention (Cross-Attention):**\n\n```\nQ = output masked self-attention của decoder  ← đến từ DECODER\nK = output encoder stack cuối cùng            ← đến từ ENCODER\nV = output encoder stack cuối cùng            ← đến từ ENCODER\n```\n\nĐây là **cầu nối** giữa encoder và decoder. Decoder “hỏi” encoder: *“Để sinh token output tiếp theo, tôi cần chú ý phần nào của input?”*\n\nVí dụ dịch “I love AI” → “Tôi yêu AI”: Khi decoder chuẩn bị sinh “yêu”, cross-attention giúp nó tập trung vào $z_2$ (biểu diễn của “love” từ encoder).\n\n![Multi-Head Attention](/api/uploads/606c0e2a4d2c4dba8584.png)\n\n*Hình: 8 head output được concat rồi nhân $W^O$ để ra vector cuối cùng*\n\n---\n\n# 8\\. Feed-Forward Network, Residual và Layer Norm\n\n## 8.1. Position-wise Feed-Forward Network\n\nSau attention, mỗi vị trí đi qua một mạng FFN **độc lập và giống nhau**:\n\n$\\text{FFN}(x) = \\max(0, xW_1 + b_1) \\cdot W_2 + b_2$\n\n-   Lớp 1: project $512 \\to 2048$ (mở rộng 4 lần)\n-   ReLU activation (các giá trị âm → 0)\n-   Lớp 2: project $2048 \\to 512$ (thu nhỏ lại)\n\n**Tại sao cần FFN sau attention?**\n\nSelf-attention thực chất là phép tuyến tính (nhân ma trận) + softmax. Nếu chỉ stack nhiều layer attention, mô hình vẫn bị giới hạn về khả năng biểu diễn. FFN thêm **tính phi tuyến** (qua ReLU) — cho phép mô hình học các pattern phức tạp hơn.\n\nCách hiểu trực quan: attention quyết định **“nên chú ý đến đâu”**, FFN quyết định **“làm gì với thông tin đã thu thập được”**.\n\n**Tại sao “position-wise”?** FFN áp dụng **cùng tham số** ($W_1, b_1, W_2, b_2$) cho mọi vị trí token trong câu, nhưng xử lý từng token **độc lập**. Token “Mèo” và token “thảm” đi qua cùng FFN nhưng input khác nhau → output khác nhau. Giữa các layer khác nhau, FFN dùng **tham số khác nhau**.\n\n**Ví dụ số:**\n\nGiả sử input của FFN cho token “Mèo” là vector $x = [0.5, -0.3, 0.8, 0.1]$ (4-dim thay vì 512 để dễ hiểu):\n\n```\nLớp 1: z = x · W₁ + b₁ = [1.2, -0.5, 2.1, 0.3, -1.4, 0.8, ...]   (8-dim thay vì 2048)\nReLU:  h = max(0, z)   = [1.2,  0.0, 2.1, 0.3,  0.0, 0.8, ...]   (giá trị âm → 0)\nLớp 2: y = h · W₂ + b₂ = [0.7, -0.1, 0.9, 0.2]                    (4-dim trở lại)\n```\n\nKết quả $y$ có cùng kích thước với input $x$ → có thể cộng residual.\n\n## 8.2. Residual Connection (Kết nối tắt)\n\nÝ tưởng từ ResNet (He et al., 2016): thay vì $y = f(x)$, ta dùng $y = x + f(x)$.\n\n**Tại sao cần residual?** Hãy tưởng tượng bạn xây một cầu thang 6 tầng (6 encoder layer). Nếu mỗi tầng chỉ có cầu thang (sub-layer), thông tin phải đi qua **từng bậc** — dễ bị thất lạc hoặc biến dạng. Residual connection giống như lắp **thang máy** bên cạnh cầu thang: thông tin có thể đi thẳng từ tầng 1 lên tầng 6, trong khi cầu thang bổ sung thêm thông tin mới ở mỗi tầng.\n\nCụ thể hơn:\n\n```\nKhông residual:  x → Attention(x) → FFN(Attention(x)) → ...\nCó residual:     x → x + Attention(x) → (x+Attn) + FFN(x+Attn) → ...\n```\n\nLợi ích:\n\n-   **Gradient flow trực tiếp** từ output về input — khi backprop, gradient có thể “đi thang máy” về layer đầu mà không bị nhân qua nhiều ma trận → tránh vanishing gradient khi stack 6+ layer\n-   **Bảo toàn thông tin gốc** — nếu sub-layer chưa học tốt ở epoch đầu, ít nhất $x$ vẫn đi qua nguyên vẹn → training ổn định ngay từ đầu\n-   **Identity mapping** — trường hợp worst case: sub-layer output bằng 0 → $y = x$ → không gây hại\n\n## 8.3. Layer Normalization\n\nSau residual, áp dụng Layer Normalization — chuẩn hoá **theo chiều feature** (tất cả 512 chiều) cho từng token, độc lập batch size.\n\n**Cách tính:** Cho vector $x$ của 1 token (512 chiều):\n\n1.  Tính mean: $\\mu = \\frac{1}{512}\\sum_{i=1}^{512} x_i$\n2.  Tính variance: $\\sigma^2 = \\frac{1}{512}\\sum_{i=1}^{512}(x_i - \\mu)^2$\n3.  Normalize: $\\hat{x}_i = \\frac{x_i - \\mu}{\\sqrt{\\sigma^2 + \\epsilon}}$\n4.  Scale và shift: $y_i = \\gamma \\cdot \\hat{x}_i + \\beta$ (với $\\gamma$, $\\beta$ là tham số học được)\n\n**Tại sao cần normalize?** Sau attention + residual, giá trị trong vector có thể vary rất lớn (vài chiều = 100, vài chiều = 0.01). LayerNorm đưa mọi chiều về **cùng scale** → giúp layer tiếp theo xử lý ổn định hơn, tốc độ hội tụ nhanh hơn.\n\n**Tại sao không dùng Batch Normalization?**\n\n-   **BatchNorm** chuẩn hoá theo **batch dimension** — với mỗi feature, tính mean/variance **qua tất cả sample trong batch**. Vấn đề: khi sequence length khác nhau giữa các sample (câu dài câu ngắn), thống kê batch không ổn định. Khi inference (batch size = 1), phải dùng running statistics → không chính xác.\n-   **LayerNorm** chuẩn hoá theo **feature dimension** — mỗi sample (mỗi token) tự normalize riêng, **không phụ thuộc batch size**. Phù hợp hoàn hảo cho sequence model với variable length.\n\n## 8.4. Tổng hợp: Một token đi qua 1 Encoder layer\n\n![8.4. Tổng hợp: Một token đi qua 1 Encoder layer](/api/uploads/45da4e4a6b2db69165d6.png)\n\n## 8.5. Tổng hợp: Một token đi qua 1 Decoder layer\n\nDecoder layer phức tạp hơn — có 3 sub-layer thay vì 2:\n\n![8.5. Tổng hợp: Một token đi qua 1 Decoder layer](/api/uploads/43dcea44643378defbff.png)\n\n## 8.6. Ví dụ end-to-end: “I love AI” đi qua toàn bộ Transformer\n\nHãy theo dõi chi tiết luồng dữ liệu cho bài toán dịch “I love AI” → “Tôi yêu AI”:\n\n**Phía Encoder:**\n\n```\nBước 1: Tokenize → [\"I\", \"love\", \"AI\"] → ID [42, 4523, 891]\n\nBước 2: Embedding lookup → 3 vector × 512 chiều\n  x₁ = Embed(42),  x₂ = Embed(4523),  x₃ = Embed(891)\n  → nhân √512 ≈ 22.6\n\nBước 3: Cộng Positional Encoding\n  z₁ = x₁×22.6 + PE(pos=0),  z₂ = x₂×22.6 + PE(pos=1),  z₃ = x₃×22.6 + PE(pos=2)\n\nBước 4: Encoder Layer 1\n  4a. Self-Attention: z₁ attend z₂, z₃ (và chính nó)\n      → biết \"I\" đi với \"love\" và \"AI\"\n  4b. Add & Norm: z₁' = LayerNorm(z₁ + Attn(z₁))\n  4c. FFN: biến đổi phi tuyến\n  4d. Add & Norm: z₁'' = LayerNorm(z₁' + FFN(z₁'))\n\nBước 5: Lặp lại cho Encoder Layer 2, 3, 4, 5, 6\n  → Output cuối: e₁, e₂, e₃ (3 vector × 512)\n  Mỗi vector giờ chứa thông tin ngữ cảnh TOÀN CÂU\n```\n\n**Phía Decoder (sinh token “yêu” — token thứ 2):**\n\n```\nInput decoder: [\"<start>\", \"Tôi\"] (đã sinh trước đó)\n\nBước 1: Embedding + PE → d₁, d₂\n\nBước 2: Masked Self-Attention\n  \"Tôi\" chỉ attend [\"<start>\", \"Tôi\"] (mask token tương lai)\n  → d₂' = ngữ cảnh output đã sinh\n\nBước 3: Cross-Attention\n  Q = d₂' (từ decoder)\n  K, V = e₁, e₂, e₃ (từ encoder — biểu diễn \"I\", \"love\", \"AI\")\n  → \"Tôi\" chú ý mạnh vào e₂ (\"love\") → cross_out\n\nBước 4: FFN + Add & Norm → vector cuối cho vị trí 2\n\nBước 5: Linear (512 → vocab_size) + Softmax\n  → Token có xác suất cao nhất: \"yêu\" ✓\n```\n\nQuá trình này lặp lại: thêm “yêu” vào input decoder → sinh “AI” → sinh \"\" → dừng.\n\n---\n\n# 9\\. Tại sao Self-Attention?\n\nBài báo so sánh self-attention với recurrent và convolutional layer trên 3 tiêu chí.\n\n| Layer | Complexity/Layer | Sequential Ops | Max Path Length |\n| --- | --- | --- | --- |\n| **Self-Attention** | $O(n^2 \\cdot d)$ | $O(1)$ | $O(1)$ |\n| Recurrent | $O(n \\cdot d^2)$ | $O(n)$ | $O(n)$ |\n| Convolutional | $O(k \\cdot n \\cdot d^2)$ | $O(1)$ | $O(\\log_k n)$ |\n| Self-Attention (restricted) | $O(r \\cdot n \\cdot d)$ | $O(1)$ | $O(n/r)$ |\n\n**Giải thích:**\n\n-   **Sequential Operations:** Self-attention cần $O(1)$ bước tuần tự (song song hoá hoàn toàn), RNN cần $O(n)$ bước\n-   **Max Path Length:** Khoảng cách tín hiệu giữa 2 token bất kỳ. Self-attention: $O(1)$ (trực tiếp). RNN: $O(n)$ (phải đi qua n hidden state). CNN: $O(\\log_k n)$ (cần stack nhiều layer)\n-   **Complexity:** Self-attention là $O(n^2 \\cdot d)$ — nhanh hơn recurrent $O(n \\cdot d^2)$ khi $n < d$, đúng với hầu hết sentence-level representation (BPE/WordPiece thường cho $n$ vài chục đến vài trăm, $d = 512$)\n\nNgoài ra, self-attention có **side benefit** về interpretability — các head học các nhiệm vụ khác nhau, có thể visualize để phân tích mô hình.\n\n## Giải thích cụ thể từng tiêu chí\n\n**1\\. Complexity per Layer:**\n\n-   Self-attention: mỗi token phải so khớp với $n$ token khác (nhân Q·K), mỗi lần tốn $O(d)$ → tổng $O(n^2 \\cdot d)$\n-   Recurrent: mỗi bước nhân ma trận $d \\times d$, lặp $n$ bước → $O(n \\cdot d^2)$\n-   Khi $n < d$ (câu 50 token, $d = 512$): $n^2 \\cdot d = 50^2 \\times 512 = 1.28M$ < $n \\cdot d^2 = 50 \\times 512^2 = 13.1M$ → self-attention nhanh hơn ~10 lần\n\n**2\\. Sequential Operations (khả năng song song hoá):**\n\n-   Self-attention: tính Q·K cho mọi cặp → **1 phép matmul song song** → $O(1)$ bước tuần tự\n-   Recurrent: $h_1 \\to h_2 \\to ... \\to h_n$ → **$n$ bước nối tiếp**, không thể song song\n\nĐây là lý do Transformer train nhanh hơn RNN đáng kể trên GPU/TPU hiện đại.\n\n**3\\. Maximum Path Length (quan trọng nhất cho long-range dependency):**\n\n-   Self-attention: token đầu câu → token cuối câu: **1 bước** (trực tiếp qua attention)\n-   Recurrent: phải đi qua **$n$ hidden state** → thông tin suy hao\n-   CNN: cần stack $O(\\log_k n)$ layer để 2 token xa nhau “gặp” nhau\n\nPath ngắn = gradient truyền dễ hơn = mô hình học long-range dependency tốt hơn.\n\n---\n\n# 10\\. Huấn luyện Transformer\n\n## 10.1. Dữ liệu\n\n| Task | Dataset | Số cặp câu | Tokenizer | Vocab |\n| --- | --- | --- | --- | --- |\n| Anh → Đức | WMT 2014 EN-DE | ~4.5 triệu | BPE | ~37.000 (shared) |\n| Anh → Pháp | WMT 2014 EN-FR | 36 triệu | WordPiece | 32.000 |\n\nMỗi batch chứa khoảng **25.000 source token + 25.000 target token**, các cặp câu được nhóm theo độ dài gần nhau để tối ưu padding.\n\n## 10.2. Phần cứng và thời gian\n\nToàn bộ training trên **1 máy với 8 GPU NVIDIA P100**:\n\n| Mô hình | Thời gian/step | Tổng steps | Thời gian | Config |\n| --- | --- | --- | --- | --- |\n| Base | 0.4 giây | 100.000 | **12 giờ** | $d_{model}$=512, h=8, N=6, $d_{ff}$=2048 |\n| Big | 1.0 giây | 300.000 | **3.5 ngày** | $d_{model}$=1024, h=16, N=6, $d_{ff}$=4096 |\n\n## 10.3. Optimizer\n\nDùng **Adam** với $\\beta_1 = 0.9$, $\\beta_2 = 0.98$, $\\epsilon = 10^{-9}$.\n\nLearning rate thay đổi theo công thức đặc biệt:\n\n$lr = d_{model}^{-0.5} \\cdot \\min(step^{-0.5}, step \\cdot warmup\\_steps^{-1.5})$\n\nVới $warmup\\_steps = 4000$: learning rate **tăng tuyến tính** trong 4000 step đầu (khởi động), sau đó **giảm theo nghịch đảo căn bậc hai** của step number.\n\nTrực giác: mô hình “chạy tốc độ thấp” lúc đầu (warmup) để ổn định, rồi tăng tốc, rồi giảm dần khi đã gần hội tụ.\n\n**Ví dụ cụ thể với $d_{model} = 512$, $warmup = 4000$:**\n\n```\nStep 1:     lr = 512^(-0.5) × 1 × 4000^(-1.5) ≈ 0.044 × 0.000004 ≈ 1.8e-7  (rất nhỏ)\nStep 2000:  lr tăng tuyến tính lên khoảng 5e-4\nStep 4000:  lr đạt đỉnh ≈ 1e-3  (điểm chuyển tiếp)\nStep 10000: lr bắt đầu giảm theo 1/√step\nStep 100000: lr ≈ 1.4e-4  (nhỏ dần)\n```\n\nHình dạng: giống chữ V lộn ngược — tăng nhanh rồi giảm chậm. Warmup giúp tránh “explosive gradient” ở những step đầu khi tham số còn ngẫu nhiên.\n\n## 10.4. Regularization\n\n**Residual Dropout** ($P_{drop} = 0.1$): áp dụng lên output mỗi sub-layer (trước khi cộng residual) và lên tổng embedding + positional encoding.\n\n**Label Smoothing** ($\\epsilon_{ls} = 0.1$): thay vì target là one-hot \\[0, 0, 1, 0, …\\], ta dùng \\[0.017, 0.017, 0.9, 0.017, …\\]. Điều này khiến mô hình “khiêm tốn hơn” — perplexity tăng nhẹ nhưng **accuracy và BLEU cải thiện**.\n\n**Ví dụ chi tiết:** Giả sử vocabulary có 6 token và target là token thứ 3. Không có label smoothing:\n\n```\nTarget: [0, 0, 1, 0, 0, 0]  → Mô hình bị ép output xác suất 1.0 cho token đúng\n```\n\nVới label smoothing $\\epsilon = 0.1$:\n\n```\nTarget: [0.017, 0.017, 0.9, 0.017, 0.017, 0.017]\n```\n\nMô hình được “cho phép” phân bổ 10% xác suất cho các token khác. Điều này:\n\n-   Tránh mô hình **quá tự tin** (overconfident) vào 1 token → giảm overfitting\n-   Khuyến khích phân phối xác suất **mượt hơn** → giúp beam search hoạt động tốt hơn\n-   Perplexity tăng (vì mô hình “không chắc chắn”) nhưng BLEU tăng (vì dịch chính xác hơn)\n\n---\n\n# 11\\. Kết quả thực nghiệm\n\n## 11.1. Dịch máy (Machine Translation)\n\n**BLEU score là gì?** BLEU (Bilingual Evaluation Understudy) đo mức độ trùng khớp giữa bản dịch của máy và bản dịch tham chiếu của con người. Score 0 = hoàn toàn sai, 100 = giống hệt tham chiếu. Trong thực tế, BLEU 25-30 cho EN-DE được coi là rất tốt, 40+ cho EN-FR là xuất sắc. Cải thiện 1-2 BLEU thường được xem là đáng kể.\n\n| Model | EN-DE BLEU | EN-FR BLEU | FLOPs (EN-DE) |\n| --- | --- | --- | --- |\n| ByteNet | 23.75 | — | — |\n| GNMT + RL | 24.6 | 39.92 | 2.3×10¹⁹ |\n| ConvS2S | 25.16 | 40.46 | 9.6×10¹⁸ |\n| MoE | 26.03 | 40.56 | 2.0×10¹⁹ |\n| ConvS2S Ensemble | 26.36 | 41.29 | 7.7×10¹⁹ |\n| GNMT Ensemble | 26.30 | 41.16 | 1.8×10²⁰ |\n| **Transformer (base)** | **27.3** | 38.1 | **3.3×10¹⁸** |\n| **Transformer (big)** | **28.4** | **41.8** | 2.3×10¹⁹ |\n\nTransformer big vượt **mọi mô hình trước đó** — kể cả ensemble — trên cả hai ngôn ngữ, với chi phí training chỉ bằng một phần nhỏ. EN-FR 41.8 BLEU đạt được chỉ trong 3.5 ngày trên 8 GPU.\n\nInference: beam search với beam size 4, length penalty $\\alpha = 0.6$. Base model average 5 checkpoint cuối, big model average 20 checkpoint cuối.\n\n## 11.2. Ablation Study — Điều gì quan trọng?\n\nBài báo thay đổi từng yếu tố của base model để đánh giá tác động — đây là phần cực kỳ giá trị vì giúp hiểu **mỗi design choice đóng góp bao nhiêu**.\n\n**Số attention head (h):**\n\n```\nh=1  (single-head, d_k=512): BLEU 24.9  (-0.9 so với base)\nh=4  (d_k=128):              BLEU 25.5\nh=8  (d_k=64, base):         BLEU 25.8  ← tốt nhất\nh=16 (d_k=32):               BLEU 25.8  (bằng base)\nh=32 (d_k=16):               BLEU 25.4  (-0.4)\n```\n\nNhận xét: Single-head kém rõ rệt → multi-head quan trọng. Nhưng quá nhiều head (32) cũng giảm vì mỗi head chỉ còn 16 chiều — quá nhỏ để biểu diễn pattern phức tạp.\n\n**Kích thước key ($d_k$):**\n\n```\nd_k=16: BLEU 25.1  (-0.7)\nd_k=32: BLEU 25.4  (-0.4)\nd_k=64 (base): BLEU 25.8\n```\n\nGiảm $d_k$ → giảm quality. Bài báo nhận xét: hàm compatibility (dot product) cần đủ không gian để phân biệt các kiểu liên hệ → d\\_k nhỏ quá thì mô hình “mù màu”.\n\n**Số layer (N):**\n\n```\nN=2: BLEU 23.7, params 36M  → quá nông, giảm 2.1 BLEU\nN=4: BLEU 25.3, params 50M\nN=6: BLEU 25.8, params 65M  (base)\nN=8: BLEU 25.5, params 80M  → diminishing returns, có thể overfitting\n```\n\n**Kích thước model:** $d_{model} = 1024$, $d_{ff} = 4096$ → BLEU 26.0 (168M params, +0.2 so với base 65M). Model lớn hơn tốt hơn — nhưng chi phí compute tăng đáng kể.\n\n**Dropout:** Bỏ dropout ($P_{drop} = 0.0$) → giảm 1.2 BLEU → overfitting nghiêm trọng. $P_{drop} = 0.2$ → tương đương base. Kết luận: **dropout là bắt buộc**.\n\n**Label smoothing:** Bỏ ($\\epsilon_{ls} = 0.0$) → BLEU 25.3 (-0.5). Tăng ($\\epsilon_{ls} = 0.2$) → BLEU 25.7. Smoothing 0.1 cho cân bằng tốt nhất.\n\n**Positional encoding:** Learned embedding: BLEU 25.7 vs sinusoidal 25.8 → **gần như giống nhau**. Tác giả chọn sinusoidal vì khả năng extrapolation.\n\n## 11.3. English Constituency Parsing\n\nĐể chứng minh Transformer tổng quát hoá tốt, bài báo thử trên bài toán **phân tích cú pháp tiếng Anh** — một task rất khác dịch máy:\n\n| Parser | Setting | F1 Score |\n| --- | --- | --- |\n| Petrov et al. 2006 | WSJ only | 90.4 |\n| Dyer et al. 2016 | WSJ only | 91.7 |\n| **Transformer (4 layers)** | **WSJ only** | **91.3** |\n| Vinyals & Kaiser 2014 | semi-supervised | 92.1 |\n| **Transformer (4 layers)** | **semi-supervised** | **92.7** |\n\nTransformer vượt BerkeleyParser dù chỉ train trên 40K câu WSJ, và không có task-specific tuning.\n\n## 11.4. Attention Visualizations\n\nBài báo cung cấp visualizations cho thấy các head **tự học** các nhiệm vụ khác nhau mà không cần supervision:\n\n**Ví dụ 1 — Long-distance dependencies (Figure 3 trong paper):**\n\nCâu: *“It is in this spirit that a majority of American governments have passed new laws since 2009 making the registration or voting process more difficult.”*\n\nKhi encode token “making” ở layer 5, nhiều attention head tự động attend mạnh đến “more” và “difficult” — hoàn thành cụm “making…more difficult” dù hai cụm này cách nhau nhiều token. Đây là bằng chứng trực quan rằng self-attention capture long-range dependency hiệu quả.\n\n**Ví dụ 2 — Anaphora resolution (Figure 4 trong paper):**\n\nCâu: *“The Law will never be perfect, but its application should be just.”*\n\nHead 5 và 6 ở layer 5 khi xử lý token “its” tạo attention rất **“sharp”** (tập trung cao) vào “Law” — tức mô hình tự học được “its” ám chỉ “Law”. Điều đáng chú ý: không ai dạy mô hình grammar hay coreference — nó tự khám phá từ dữ liệu dịch thuật.\n\n**Ví dụ 3 — Structural pattern (Figure 5 trong paper):**\n\nHai head khác nhau ở cùng layer 5 học hai pattern cấu trúc khác nhau: một head theo dõi quan hệ subject-verb, head khác theo dõi quan hệ modifier-noun. Điều này minh hoạ tại sao multi-head quan trọng — mỗi head chuyên về một kiểu quan hệ.\n\n---\n\n# 12\\. Từ Transformer đến Pre-trained Language Models\n\n## 12.1. Paradigm shift: Pre-train + Fine-tune\n\nTransformer gốc được thiết kế cho dịch máy — train từ đầu cho mỗi task. Năm 2018, hai nhóm nghiên cứu nhận ra rằng Transformer có thể dùng theo cách **mạnh hơn nhiều**:\n\n1.  **Pre-train** trên corpus văn bản khổng lồ (không cần nhãn) → mô hình học ngữ pháp, ngữ nghĩa, kiến thức thế giới\n2.  **Fine-tune** trên task cụ thể với ít dữ liệu có nhãn → chuyển giao kiến thức đã học\n\n## 12.2. Tại sao paradigm này hiệu quả?\n\n**Dữ liệu unlabeled gần như vô hạn:** Internet chứa hàng trăm tỷ từ — Wikipedia, sách, blog, code, diễn đàn… Không bộ dữ liệu có nhãn nào (thường chỉ vài nghìn đến vài triệu mẫu) sánh được về quy mô và đa dạng.\n\n**Ngôn ngữ chứa kiến thức ngầm:** Chỉ từ việc dự đoán từ tiếp theo (hoặc từ bị che), mô hình buộc phải học:\n\n-   **Ngữ pháp:** “She \\_\\_ to the store” → “went” (chia động từ đúng)\n-   **Sự kiện:** “The capital of France is \\_\\_” → “Paris” (kiến thức thế giới)\n-   **Logic:** “If it rains, the ground gets \\_\\_” → “wet” (nhân quả)\n-   **Ngữ nghĩa:** “happy” gần “joyful”, xa “sad” (quan hệ nghĩa)\n\n**Transfer learning:** Tham số đã pre-train trở thành **khởi điểm tuyệt vời** cho mọi task downstream. Thay vì train 100% từ random weights, fine-tune chỉ cần điều chỉnh nhẹ — tiết kiệm data, thời gian, và compute.\n\n## 12.3. Hai nhánh tiến hoá\n\n![12.3. Hai nhánh tiến hoá](/api/uploads/9a3612d9204f4746de75.png)\n\n![12.3. Hai nhánh tiến hoá](/api/uploads/e632b2de8492f17adac4.png)\n\n---\n\n# 13\\. GPT — Decoder-Only\n\n## 13.1. Tổng quan\n\n**GPT** (Generative Pre-trained Transformer) được OpenAI công bố tháng 6/2018 trong bài báo *“Improving Language Understanding by Generative Pre-Training”* bởi Radford et al.\n\nÝ tưởng cốt lõi: lấy phần **Decoder** của Transformer, bỏ hẳn Encoder và cross-attention, chỉ giữ **masked self-attention + FFN**, rồi pre-train trên lượng lớn văn bản.\n\n## 13.2. Kiến trúc\n\n| Thông số | GPT-1 |\n| --- | --- |\n| Số layer | 12 decoder block |\n| $d_{model}$ | 768 |\n| Attention heads | 12 |\n| FFN inner dim | 3072 |\n| Tổng tham số | ~117M |\n| Max sequence | 512 token |\n| Activation | GELU (thay ReLU) |\n| Positional encoding | Learned (không dùng sin-cos) |\n| Weight tying | Có (embedding = output projection) |\n\nMỗi block chỉ gồm **2 sub-layer**: masked self-attention + FFN (không có cross-attention vì không có encoder).\n\n**Tại sao GELU thay ReLU?** ReLU ($\\max(0, x)$) “cắt” hoàn toàn giá trị âm về 0 — quá “cứng”. GELU (Gaussian Error Linear Unit) cho phép một số giá trị âm nhỏ đi qua, tạo gradient mượt hơn → huấn luyện ổn định hơn cho language model.\n\n**Weight tying:** Ma trận token embedding $W_e$ được **dùng lại** cho lớp output projection (nhân với hidden state cuối cùng rồi softmax). Điều này giảm 30M+ tham số và buộc embedding phải học biểu diễn hữu ích cho cả input lẫn output.\n\n## 13.3. Causal Self-Attention — Tại sao cần mask?\n\nGPT dự đoán token tiếp theo: cho trước $w_1, w_2, ..., w_t$, dự đoán $w_{t+1}$.\n\nNếu token $w_t$ “nhìn thấy” $w_{t+1}$ khi training, nó chỉ cần copy — không học được gì. Do đó, **causal mask** buộc mỗi token chỉ attend token phía trước:\n\n```\n      w₁   w₂   w₃   w₄   w₅\nw₁  [ O    X    X    X    X  ]\nw₂  [ O    O    X    X    X  ]\nw₃  [ O    O    O    X    X  ]\nw₄  [ O    O    O    O    X  ]\nw₅  [ O    O    O    O    O  ]\n\nO = attend được    X = bị chặn (-∞)\n```\n\nQuan trọng: BERT dùng **full attention** (toàn O) — hiểu được hai chiều nhưng không sinh được. GPT dùng **lower-triangular** — sinh được auto-regressively.\n\n## 13.4. Pre-training: Causal Language Modeling\n\n**Dữ liệu:** BooksCorpus — khoảng 7.000 cuốn sách chưa xuất bản, ~800 triệu từ. Được chọn vì chứa văn xuôi dài, giàu ngữ cảnh xa.\n\n**Mục tiêu:** Maximize xác suất token tiếp theo cho mỗi vị trí:\n\n$\\mathcal{L}_1(U) = \\sum_i \\log P(u_i \\mid u_{i-k}, ..., u_{i-1}; \\Theta)$\n\nCách hiểu: cho N token liên tiếp, mô hình có N-1 bài tập dự đoán — rất hiệu quả về dữ liệu.\n\n**Quá trình processing:**\n\n1.  Token + Learned Position Embedding → $h_0 = U \\cdot W_e + W_p$\n2.  Đi qua 12 decoder block: $h_l = \\text{TransformerBlock}(h_{l-1})$\n3.  Output: $P(u) = \\text{softmax}(h_{12} \\cdot W_e^T)$ — dùng lại ma trận embedding cho output (weight tying)\n\n**Hardware:** 8 GPU, training khoảng 1 tháng, 100 epochs, batch 64 chuỗi × 512 token.\n\n## 13.5. Fine-tuning: Input Transformations\n\nThay vì thiết kế kiến trúc riêng cho mỗi task, GPT biến mọi task thành cùng format “chuỗi token → dự đoán”:\n\n-   **Classification:** `[Start] text [Extract]` → hidden của \\[Extract\\] → linear → softmax\n-   **Entailment:** `[Start] premise [Delim] hypothesis [Extract]`\n-   **Similarity:** Chạy 2 lần với thứ tự đảo, cộng hidden → linear\n-   **Multiple Choice:** Cho mỗi đáp án k: `[Start] context [Delim] answer_k [Extract]`, softmax qua các k\n\n**Ví dụ cụ thể — Multiple Choice:**\n\nCâu hỏi: “Thủ đô Việt Nam là?” với 4 đáp án: A) Hà Nội, B) Đà Nẵng, C) TP.HCM, D) Huế\n\nGPT xử lý 4 chuỗi song song:\n\n```\n[Start] Thủ đô Việt Nam là? [Delim] Hà Nội [Extract]   → score A = 0.85\n[Start] Thủ đô Việt Nam là? [Delim] Đà Nẵng [Extract]  → score B = 0.05\n[Start] Thủ đô Việt Nam là? [Delim] TP.HCM [Extract]   → score C = 0.07\n[Start] Thủ đô Việt Nam là? [Delim] Huế [Extract]      → score D = 0.03\n```\n\nSoftmax qua 4 score → chọn đáp án A. Điểm đẹp: **cùng 1 architecture** xử lý được classification, entailment, similarity, và multiple choice — chỉ thay cách format input.\n\nLoss fine-tuning kết hợp task loss và LM loss để tránh “quên” kiến thức pre-train:\n\n$\\mathcal{L}_3(C) = \\mathcal{L}_2(C) + \\lambda \\cdot \\mathcal{L}_1(C), \\lambda = 0.5$\n\n## 13.6. Kết quả GPT-1\n\nVượt SOTA trên 9/12 benchmark tại thời điểm công bố:\n\n| Benchmark | Task | Trước GPT | GPT-1 | Cải thiện |\n| --- | --- | --- | --- | --- |\n| Story Cloze | Commonsense reasoning | 77.6% | **86.5%** | +8.9% |\n| RACE | Reading comprehension | 53.3% | **59.0%** | +5.7% |\n| SciTail | Science QA | 83.3% | **88.3%** | +5.0% |\n| MultiNLI | Natural Language Inference | 80.6% | **82.1%** | +1.5% |\n| QQP | Paraphrase detection | 66.1% | **70.3%** | +4.2% |\n| GLUE | NLU tổng hợp | 68.9 | **72.8** | +3.9 |\n\nAblation: bỏ pre-training → giảm trung bình **14.8%** — chứng minh giá trị to lớn của pre-training.\n\n## 13.7. Zero-shot capability — Điều bất ngờ\n\nMột phát hiện thú vị: GPT-1 đạt hiệu suất hợp lý trên một số task **mà không cần fine-tune** (zero-shot). Khi số bước pre-training tăng, hiệu suất zero-shot cũng tăng liên tục — cho thấy causal LM tự nhiên học được khả năng suy luận. Khám phá này mở đường cho GPT-2 và GPT-3, nơi zero-shot/few-shot trở thành paradigm chính.\n\n## 13.8. GPT trong bức tranh lớn\n\nGPT-1 chỉ là khởi đầu. Cùng kiến trúc decoder-only, OpenAI scale lên:\n\n-   **GPT-2** (2019): 1.5 tỷ tham số, train trên 40GB WebText, context 1024 token\n-   **GPT-3** (2020): 175 tỷ tham số, few-shot learning đột phá\n-   **ChatGPT** (2022): GPT-3.5 + RLHF (Reinforcement Learning from Human Feedback) → chatbot “hiểu” instruction\n-   **GPT-4** (2023): multimodal (text + image), reasoning mạnh hơn\n\nKiến trúc cốt lõi gần như **không thay đổi** — chỉ scale params + data + thêm alignment.\n\n---\n\n# 14\\. BERT — Encoder-Only\n\n## 14.1. Tổng quan\n\n**BERT** (Bidirectional Encoder Representations from Transformers) được Google công bố tháng 10/2018 bởi Devlin et al. Nếu GPT là “người viết” (sinh văn bản), BERT là “người đọc” (hiểu văn bản).\n\nĐóng góp cốt lõi: chứng minh rằng **pre-training hai chiều (bidirectional)** vượt trội hơn một chiều (left-to-right như GPT) cho các task hiểu ngôn ngữ.\n\n## 14.2. Kiến trúc\n\nBERT dùng **encoder stack** của Transformer gốc — **full bidirectional self-attention**, không có mask:\n\n| Thông số | BERT-Base | BERT-Large |\n| --- | --- | --- |\n| Số layer (L) | 12 | 24 |\n| Hidden size (H) | 768 | 1024 |\n| Attention heads | 12 | 16 |\n| FFN inner dim | 3072 | 4096 |\n| Tổng tham số | ~110M | ~340M |\n| Max sequence | 512 | 512 |\n| Activation | GELU | GELU |\n| Positional encoding | Learned | Learned |\n\n## 14.3. Input Representation — Ba lớp embedding cộng lại\n\nĐây là một trong những thiết kế thông minh nhất của BERT. Mỗi token có input embedding là **tổng** của 3 embedding khác nhau, mỗi cái mang một loại thông tin:\n\n```\n Token Emb:   [CLS]    my     dog    [SEP]    he    cute   [SEP]\n + Segment:    E_A     E_A    E_A     E_A    E_B    E_B    E_B\n + Position:   E_0     E_1    E_2     E_3    E_4    E_5    E_6\n ─────────────────────────────────────────────────────────────\n = Final Input (768-dim cho mỗi token)\n```\n\n**Token Embedding:** giống Transformer gốc — ánh xạ token ID → vector 768-dim. Biết token đó **là gì** (ý nghĩa ngữ nghĩa).\n\n**Segment Embedding:** BERT thường nhận input là **cặp câu** (cho NSP, NLI, QA). Segment embedding phân biệt token thuộc câu A ($E_A$) hay câu B ($E_B$). Chỉ có **2 vector** segment embedding được học.\n\n**Position Embedding:** giống GPT, dùng **learned** positional embedding (không dùng sin-cos). Tối đa 512 vị trí.\n\n**Token đặc biệt:**\n\n-   **\\[CLS\\]** (Classification): luôn ở vị trí đầu tiên. Hidden state của \\[CLS\\] sau khi đi qua 12 layer encoder trở thành **biểu diễn toàn câu** — dùng cho sentence classification. Tại sao \\[CLS\\] đại diện được cả câu? Vì qua self-attention, \\[CLS\\] attend đến **mọi token** trong câu → “tổng hợp” thông tin toàn bộ.\n-   **\\[SEP\\]** (Separator): phân cách giữa hai câu/segment.\n\n**So sánh với Transformer gốc:** Transformer gốc chỉ có Token Embedding + Positional Encoding (sin-cos). BERT thêm Segment Embedding và đổi sang learned position.\n\n## 14.4. Pre-training Task 1: Masked Language Model (MLM)\n\nGPT chỉ dự đoán left-to-right → không tận dụng ngữ cảnh phía phải. BERT giải quyết bằng cách **che (mask) ngẫu nhiên** một số token và dự đoán chúng dựa trên ngữ cảnh **hai chiều**.\n\n**Sự khác biệt cốt lõi với GPT:**\n\nHãy xem câu: *“The cat sat on the mat”*\n\nKhi dự đoán “sat”:\n\n-   **GPT (unidirectional):** chỉ nhìn “The cat” → ngữ cảnh hạn chế\n-   **BERT (bidirectional):** nhìn “The cat \\_\\_ on the mat” → biết cả **ai ngồi** (cat) và **ngồi ở đâu** (mat)\n\nĐây là lý do BERT hiểu ngôn ngữ tốt hơn GPT ở hầu hết task NLU.\n\n**Quy trình:**\n\n1.  Chọn ngẫu nhiên **15%** token trong mỗi chuỗi\n2.  Trong 15% đó:\n    -   **80%** thay bằng `[MASK]`\n    -   **10%** thay bằng token ngẫu nhiên\n    -   **10%** giữ nguyên (không đổi)\n\n**Ví dụ:**\n\n```\nGốc:    The  cat  sat  on  the  mat\nMasked:  The  cat  [MASK]  on  the  [MASK]\nPredict:  —    —    sat     —    —    mat\n```\n\n**Tại sao tỉ lệ 80/10/10?** Nếu luôn thay bằng `[MASK]`, mô hình chỉ học dự đoán khi thấy `[MASK]` — nhưng khi fine-tune, không có token `[MASK]` nào. 10% random + 10% giữ nguyên buộc mô hình **luôn phải suy luận** cho mọi token, giảm mismatch giữa pre-train và fine-tune.\n\n## 14.5. Pre-training Task 2: Next Sentence Prediction (NSP)\n\nNhiều task cần hiểu **quan hệ giữa hai câu** (QA cần hiểu câu hỏi + passage, NLI cần hiểu premise + hypothesis). BERT pre-train thêm task dự đoán câu B có thật sự nối tiếp câu A không:\n\n-   **50% IsNext:** Câu B thực sự đứng sau câu A trong corpus\n-   **50% NotNext:** Câu B được chọn ngẫu nhiên\n\n```\nIsNext:   [CLS] Anh ấy đi chợ. [SEP] Anh ấy mua sữa. [SEP] → IsNext ✓\nNotNext:  [CLS] Anh ấy đi chợ. [SEP] Chim cánh cụt sống ở Nam Cực. [SEP] → NotNext ✗\n```\n\nHidden state của token \\[CLS\\] được đưa qua classifier → dự đoán IsNext / NotNext.\n\n**Lưu ý quan trọng:** Các nghiên cứu sau (RoBERTa, 2019) cho thấy NSP **không nhất thiết cần thiết** — RoBERTa bỏ NSP và thay bằng dynamic masking, kết quả còn tốt hơn BERT gốc. Tuy nhiên, NSP vẫn là đóng góp có ý nghĩa trong bối cảnh 2018.\n\n## 14.6. Training Setup\n\n| Thông số | Chi tiết |\n| --- | --- |\n| Dữ liệu | BooksCorpus (800M từ) + English Wikipedia (2.500M từ) = **~3.3 tỷ từ** |\n| Hardware | 4× Cloud TPU v3 Pod (16 TPU chip) |\n| Thời gian | **4 ngày** |\n| Batch size | 256 chuỗi × 512 token |\n| Optimizer | Adam, lr=1e-4, warmup 10.000 steps |\n| Dropout | 0.1 trên mọi layer |\n\nSo với GPT-1: BERT dùng corpus **gấp 4 lần** (3.3B vs 800M từ) và hardware mạnh hơn (TPU vs GPU).\n\n## 14.7. Fine-tuning\n\nBERT fine-tune **toàn bộ mô hình** cho mỗi task, chỉ thêm 1 output layer:\n\n| Task | Input Format | Output |\n| --- | --- | --- |\n| Sentence Classification | `[CLS] text [SEP]` | h\\[CLS\\] → Linear → Softmax |\n| NLI / Entailment | `[CLS] sent_A [SEP] sent_B [SEP]` | h\\[CLS\\] → Linear(3) |\n| QA (SQuAD) | `[CLS] question [SEP] passage [SEP]` | Mỗi token → start/end logits |\n| NER | `[CLS] tok₁ tok₂ ... [SEP]` | Mỗi token → class label |\n\nFine-tune rất nhanh: SQuAD chỉ cần ~30 phút trên 1 Cloud TPU.\n\n**Ví dụ cụ thể — QA trên SQuAD:**\n\n```\nInput:  [CLS] Thủ đô Việt Nam là gì? [SEP] Hà Nội là thủ đô và thành phố \n        lớn nhất Việt Nam. Thành phố nằm ở đồng bằng sông Hồng. [SEP]\n```\n\nBERT thêm 2 vector start (S) và end (E) có kích thước 768. Với mỗi token trong passage, tính:\n\n-   P(start) = softmax(hidden\\_token · S)\n-   P(end) = softmax(hidden\\_token · E)\n\nGiả sử P(start) cao nhất ở “Hà” và P(end) cao nhất ở “Nội” → đáp án: **“Hà Nội”**.\n\nChỉ cần train 2 vector (S, E) + fine-tune lại BERT → đạt F1 93.2 trên SQuAD v1.1, vượt con người (91.2).\n\n## 14.8. Kết quả BERT\n\nBERT phá kỷ lục trên hầu hết benchmark NLU tại thời điểm công bố:\n\n| Benchmark | GPT-1 | BERT-Base | BERT-Large |\n| --- | --- | --- | --- |\n| GLUE (avg) | 72.8 | 78.3 | **80.5** |\n| MultiNLI | 82.1 | 84.6 | **86.7** |\n| SST-2 | 91.3 | 93.5 | **94.9** |\n| CoLA | 45.4 | 52.1 | **60.5** |\n| SQuAD v1.1 F1 | — | 88.5 | **93.2** |\n| SQuAD v2.0 F1 | — | — | **83.1** |\n| SWAG | — | 81.6 | **86.3** |\n\nĐặc biệt ấn tượng: **SQuAD v1.1 F1 = 93.2** — vượt hiệu suất con người (91.2).\n\n## 14.9. Tại sao BERT hiệu quả đến vậy?\n\n**1\\. Bidirectional context là chìa khoá.** Ablation study trong paper cho thấy: khi đổi BERT thành left-to-right (giống GPT), GLUE giảm **4 điểm**, SQuAD F1 giảm **7 điểm**. Chứng minh rõ ràng: nhìn hai chiều >> một chiều cho các task hiểu ngôn ngữ.\n\n**2\\. MLM buộc mô hình suy luận sâu.** Để dự đoán từ bị che, mô hình không chỉ cần hiểu ngữ pháp mà còn cần hiểu **ngữ nghĩa, logic, và cả kiến thức thế giới**. Ví dụ:\n\n```\n\"The [MASK] of France is Paris.\" → \"capital\" (kiến thức thế giới)\n\"She [MASK] very happy yesterday.\" → \"was\" (ngữ pháp - chia thì)  \n\"The cat chased the [MASK] up the tree.\" → \"bird\"/\"squirrel\" (common sense)\n```\n\n**3\\. Corpus lớn + compute mạnh.** 3.3 tỷ từ + 16 TPU chip × 4 ngày = mô hình “đọc” một lượng văn bản khổng lồ, học được pattern ngôn ngữ đa dạng.\n\n## 14.10. BERT trong thực tế\n\nSau khi công bố, BERT nhanh chóng trở thành **backbone tiêu chuẩn** cho hàng loạt ứng dụng:\n\n-   **Google Search** (2019): Google tích hợp BERT vào search engine để hiểu truy vấn tốt hơn. Ví dụ: query “2019 brazil traveler to usa need a visa” — trước BERT, Google không hiểu “to” (đi đến usa, không phải từ usa) → kết quả sai. Sau BERT → hiểu đúng hướng di chuyển.\n-   **Phân tích cảm xúc:** Fine-tune trên SST-2 → đạt 94.9% accuracy\n-   **Hệ thống hỏi đáp:** SQuAD v1.1 F1 93.2 — thay thế hàng loạt hệ thống QA truyền thống\n-   **NER (Named Entity Recognition):** Nhận diện tên người, tổ chức, địa điểm trong văn bản\n-   **Tiếng Việt:** VinAI phát triển **PhoBERT** — pre-train trên 20GB text tiếng Việt, đạt SOTA trên nhiều task NLP tiếng Việt\n\n---\n\n# 15\\. So sánh và Timeline\n\n## 15.1. Bảng so sánh Transformer vs GPT vs BERT\n\n| Tiêu chí | Transformer (2017) | GPT-1 (2018) | BERT (2018) |\n| --- | --- | --- | --- |\n| Kiến trúc | Encoder + Decoder | Decoder-only | Encoder-only |\n| Hướng attention | Enc: bidirectional, Dec: causal | Causal (trái→phải) | **Bidirectional** |\n| Mục tiêu training | Seq2Seq (dịch máy) | Causal LM | MLM + NSP |\n| Số layer | 6+6 | 12 | 12 / 24 |\n| $d_{model}$ | 512 / 1024 | 768 | 768 / 1024 |\n| Heads | 8 / 16 | 12 | 12 / 16 |\n| Tham số | 65M / 213M | 117M | 110M / 340M |\n| Activation | ReLU | GELU | GELU |\n| Positional | Sin-cos (cố định) | Learned | Learned |\n| Dataset | WMT parallel | BooksCorpus 800M từ | BooksCorpus + Wiki 3.3B từ |\n| Ứng dụng chính | Dịch máy | Generation + fine-tune | NLU, QA, classification |\n\n## 15.2. BERT vs GPT — Ai mạnh hơn?\n\nCâu trả lời: **tuỳ bài toán**. Đây không phải cuộc đua — mà là hai triết lý bổ sung cho nhau.\n\n**BERT mạnh hơn ở “hiểu”:** Bidirectional attention nghĩa là khi xử lý từ “it”, BERT nhìn được cả “The animal” (trước) lẫn “was tired” (sau). GPT chỉ nhìn được phía trước. Kết quả: BERT vượt GPT **7.7 điểm GLUE** (80.5 vs 72.8).\n\nHãy xem cụ thể:\n\n```\nCâu: \"The bank by the river was flooded.\"\n     \"The bank approved my loan.\"\n\nBERT (nhìn hai chiều): thấy \"river\" → \"bank\" = bờ sông\n                        thấy \"loan\"  → \"bank\" = ngân hàng\nGPT  (nhìn trái→phải): khi đến \"bank\", chưa thấy \"river\"/\"loan\"\n                        → phải đoán nghĩa chỉ từ ngữ cảnh trước\n```\n\nĐây là ưu thế cốt lõi của bidirectional: **hiểu nghĩa từ trong ngữ cảnh đầy đủ**.\n\n**GPT mạnh hơn ở “sinh”:** Causal attention cho phép GPT sinh văn bản token-by-token một cách tự nhiên. BERT không thể sinh vì attention nhìn cả hai chiều — không có khái niệm “token tiếp theo”.\n\nKhi bạn chat với ChatGPT hay Claude, mô hình phía sau **đọc prompt của bạn**, rồi **sinh response từng token** — token “Xin” → “chào” → ”,” → “tôi” → … Chỉ causal (decoder-only) architecture mới làm được việc này một cách tự nhiên.\n\n**Kết luận:**\n\n-   Cần **phân loại, trích xuất, search** → BERT (hoặc hậu duệ: RoBERTa, DeBERTa, PhoBERT)\n-   Cần **sinh text, chat, coding** → GPT (hoặc hậu duệ: Claude, Gemini, LLaMA)\n-   Cần **cả hai** → dùng encoder-decoder (T5, BART) hoặc decoder-only đủ lớn (GPT-4 cũng “hiểu” rất tốt nhờ scale)\n\n## 15.3. Dòng thời gian tiến hoá\n\n![15.3. Dòng thời gian tiến hoá](/api/uploads/0a586abdce10025f2690.png)\n\nKhoảng cách từ paper 2017 đến ChatGPT 2022 chỉ là **scale + data + alignment** — backbone Transformer gần như giữ nguyên.\n\n## 15.4. Các mốc quan trọng chi tiết\n\n**2017 — Transformer:** Bài báo “Attention Is All You Need” chứng minh rằng attention đủ mạnh để thay thế hoàn toàn recurrence. BLEU 28.4 trên EN-DE, training chỉ 3.5 ngày trên 8 GPU. Đặt nền móng cho mọi thứ sau này.\n\n**06/2018 — GPT-1:** OpenAI chứng minh decoder-only Transformer + pre-training trên text unlabeled → vượt SOTA trên 9/12 task. Ý tưởng then chốt: kiến thức ngôn ngữ tổng quát có thể transfer sang mọi task.\n\n**10/2018 — BERT:** Google chứng minh bidirectional pre-training >> unidirectional cho NLU. SQuAD F1 vượt con người. BERT trở thành backbone tiêu chuẩn cho search, QA, NER.\n\n**2019 — GPT-2 + RoBERTa:** OpenAI scale GPT lên 1.5B params, train trên WebText 40GB. Kết quả ấn tượng đến mức OpenAI từng “không dám” release model đầy đủ vì lo ngại misuse. Meta phát hành RoBERTa — BERT được training “đúng cách” (bỏ NSP, dynamic masking, nhiều data hơn) → vượt BERT gốc trên mọi benchmark.\n\n**2020 — GPT-3:** 175 tỷ tham số, train trên 570GB text. Khám phá “few-shot learning”: chỉ cần cho 2-3 ví dụ trong prompt, GPT-3 có thể làm task mới mà không cần fine-tune. Đây là bước ngoặt — từ “train model cho mỗi task” sang “1 model cho mọi task”.\n\n**2022 — ChatGPT:** GPT-3.5 + RLHF (Reinforcement Learning from Human Feedback). Lần đầu tiên LLM “hiểu” instruction và trả lời tự nhiên. 100 triệu user trong 2 tháng — tăng trưởng nhanh nhất lịch sử tech.\n\n**2023-2026 — Kỷ nguyên LLM:** GPT-4 (multimodal), Claude (Anthropic — safety-focused), Gemini (Google), LLaMA (Meta — open-source), DeepSeek (Trung Quốc). Kiến trúc vẫn là Transformer + vài cải tiến: RoPE, Flash Attention, GQA, MoE.\n\n---\n\n# 16\\. Kết luận\n\nTransformer không phải là “một mô hình” — nó là **khung tư duy** cho toàn bộ AI hiện đại.\n\n## Tóm tắt những đóng góp cốt lõi\n\nBài báo “Attention Is All You Need” đã chứng minh 3 điều quan trọng:\n\n1.  **Recurrence không cần thiết** cho sequence modeling — self-attention đủ mạnh để capture mọi dependency, với path length $O(1)$ thay vì $O(n)$.\n    \n2.  **Song song hoá** là chìa khoá để scale — loại bỏ bottleneck tuần tự cho phép tận dụng tối đa GPU/TPU, giảm training time từ tuần xuống giờ.\n    \n3.  **Kiến trúc đơn giản, tổng quát** — cùng một backbone (attention + residual + LayerNorm) có thể áp dụng cho dịch máy, parsing, và sau này là text generation, image recognition, audio processing, protein folding, …\n    \n\n## Tại sao Transformer thành công vượt mong đợi?\n\nKhoảng cách từ paper 2017 đến ChatGPT 2022 chỉ là **scale + data + alignment** — backbone Transformer gần như giữ nguyên. Điều này cho thấy sức mạnh của kiến trúc: nó **không hết tiềm năng** khi scale lên. Từ 65M params (Transformer gốc) → 175B params (GPT-3) → 1.8T params (GPT-4 rumored), performance vẫn tiếp tục cải thiện.\n\n## Hướng đi tiếp theo cho người học\n\nNếu bạn muốn đi sâu hơn:\n\n-   **Code lại từ đầu:** “The Annotated Transformer” (Harvard NLP) hướng dẫn implement Transformer bằng PyTorch, dòng theo dòng\n-   **Fine-tune BERT tiếng Việt:** Dùng PhoBERT (VinAI) trên dữ liệu NLP tiếng Việt\n-   **Tìm hiểu cải tiến hiện đại:** RoPE (Rotary Position Embedding), Flash Attention, Multi-Query/Grouped-Query Attention, KV Cache, Mixture of Experts\n-   **Đọc paper gốc:** Dù đã 8 năm, “Attention Is All You Need” vẫn là bài báo AI được trích dẫn nhiều nhất mọi thời đại — và mỗi lần đọc lại bạn sẽ hiểu thêm một lớp ý nghĩa mới\n\n**Attention really is all you need.**\n\n---\n\n# 17\\. Tài liệu tham khảo\n\n## Bài báo gốc\n\n1.  Vaswani, A. et al. (2017). *Attention Is All You Need*. NeurIPS. arXiv:1706.03762\n2.  Radford, A. et al. (2018). *Improving Language Understanding by Generative Pre-Training*. OpenAI.\n3.  Devlin, J. et al. (2018). *BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding*. arXiv:1810.04805\n\n## Blog giải thích trực quan\n\n4.  Jay Alammar. *The Illustrated Transformer*. [https://jalammar.github.io/illustrated-transformer/\n    \n    ](https://jalammar.github.io/illustrated-transformer/)\n5.  Jay Alammar. *The Illustrated BERT, ELMo, and co.* [https://jalammar.github.io/illustrated-bert/\n    \n    ](https://jalammar.github.io/illustrated-bert/)\n6.  Jay Alammar. *The Illustrated GPT-2*. [https://jalammar.github.io/illustrated-gpt2/\n    \n    ](https://jalammar.github.io/illustrated-gpt2/)\n7.  Harvard NLP. *The Annotated Transformer*. [http://nlp.seas.harvard.edu/2018/04/03/attention.html\n    \n    ](http://nlp.seas.harvard.edu/2018/04/03/attention.html)\n\n## Tài liệu tiếng Việt\n\n8.  AIO Conquer — AI Vietnam. *Transformer là gì? Giải thích từ gốc đến ngọn*. [https://aioconquer.aivietnam.edu.vn/posts/transformer-la-gi-giai-thich-tu-goc-den-ngon\n    \n    ](https://aioconquer.aivietnam.edu.vn/posts/transformer-la-gi-giai-thich-tu-goc-den-ngon)\n9.  Phạm Bá Cường Quốc. *Tìm hiểu mô hình Transformer*. [https://pbcquoc.github.io/transformer/\n    \n    ](https://pbcquoc.github.io/transformer/)\n10.  Phạm Bá Cường Quốc. *Attention trong Deep Learning*. [https://pbcquoc.github.io/attention/\n     \n     ](https://pbcquoc.github.io/attention/)\n11.  Phạm Đình Khánh. *Attention Is All You Need*. [https://phamdinhkhanh.github.io/2019/06/18/AttentionLayer.html\n     \n     ](https://phamdinhkhanh.github.io/2019/06/18/AttentionLayer.html)\n12.  Viblo. *Tìm hiểu về kiến trúc Transformer*. [https://viblo.asia/p/tim-hieu-ve-kien-truc-transformer-Az45byM6lxY\n     \n     ](https://viblo.asia/p/tim-hieu-ve-kien-truc-transformer-Az45byM6lxY)\n13.  200Lab. *Transformer — Công nghệ đằng sau ChatGPT*. [https://200lab.io/blog/transformer-cong-nghe-dang-sau-chatgpt-va-bard\n     \n     ](https://200lab.io/blog/transformer-cong-nghe-dang-sau-chatgpt-va-bard)\n\n## Nguồn hình ảnh\n\n-   Hình kiến trúc Transformer, Attention, Positional Encoding: Jay Alammar (CC BY-NC-SA 4.0)\n-   Hình Figure 1, 2 paper gốc: arXiv:1706.03762 (Google grants permission for scholarly use)\n-   Hình bổ sung: AIO Conquer Blog","tags":["Deep Learning","Neural Network","NLP","Transformer"]},{"slug":"gioi-thieu-batch-normalization","title":"Giới Thiệu Batch Normalization Trong Mạng Nơ-ron Nhân Tạo","date":"2025-06-22","topic":"tutorial","summary":"Trong quá trình huấn luyện mạng nơ-ron nhân tạo (neural network), các đặc trưng (features) đầu vào thường có những thang đo rất khác nhau. Ví dụ, một đặc trưng như tuổi có thể dao động từ 0 đến 100, trong khi đặc trưng như thu nhập hàng năm có thể…","body":"# Giới Thiệu Batch Normalization Trong Mạng Nơ-ron Nhân Tạo\n\n## 1\\. Đặt Vấn Đề: Tại Sao Cần Chuẩn Hóa (Normalization)?\n\nTrong quá trình huấn luyện mạng nơ-ron nhân tạo (neural network), các đặc trưng (features) đầu vào thường có những thang đo rất khác nhau. Ví dụ, một đặc trưng như tuổi có thể dao động từ 0 đến 100, trong khi đặc trưng như thu nhập hàng năm có thể dao động từ 20,000 đến 1,000,000. Sự chênh lệch này dẫn đến việc các đặc trưng có giá trị lớn hơn sẽ tạo ra gradient lớn hơn, gây ảnh hưởng không cân bằng đến quá trình cập nhật trọng số. Điều này không chỉ làm chậm quá trình hội tụ mà còn có thể khiến mạng học sai lệch, bỏ qua các đặc trưng quan trọng khác.\n\nHệ quả là quá trình huấn luyện trở nên kém hiệu quả và độ chính xác của mô hình có thể bị giảm.\n\n## 2\\. Batch Normalization Là Gì?\n\nBatch Normalization (BN) là một kỹ thuật nhằm chuẩn hóa đầu ra (activation) của từng lớp trong mạng nơ-ron. Kỹ thuật này được đề xuất như một cách để giảm hiện tượng “internal covariate shift” – tức là sự thay đổi của phân phối đầu vào tới các lớp trong quá trình huấn luyện.\n\nThay vì chuẩn hóa toàn bộ dữ liệu như trong preprocessing, BN thực hiện chuẩn hóa **trên từng batch** dữ liệu. Cụ thể, với mỗi batch, ta tính toán **trung bình (mean)** và **phương sai (variance)** của các activation, sau đó chuẩn hóa chúng về phân phối chuẩn (zero mean và unit variance). Kết quả sau chuẩn hóa sẽ tiếp tục được điều chỉnh bởi hai tham số học được là **gamma (γ)** và **beta (β)** để linh hoạt trong việc học phân phối tối ưu cho tầng tiếp theo.\n\nCông thức chuẩn hóa có thể được tóm gọn như sau:\n\n$$\n\\hat{x}^{(i)} = \\frac{x^{(i)} - \\mu_B}{\\sqrt{\\sigma_B^2 + \\epsilon}} \\qquad y^{(i)} = \\gamma \\cdot \\hat{x}^{(i)} + \\beta\n$$\n\nTrong đó:\n\n-   $\\mu_B$ và $\\sigma_B^2$ lần lượt là trung bình và phương sai của batch.\n-   $\\epsilon$ là hằng số nhỏ để tránh chia cho 0.\n-   $\\gamma$, $\\beta$ là các tham số học được trong quá trình huấn luyện.\n\n## 3\\. Vị Trí Trong Mạng và Thực Thi\n\nTrong thực tế, BN thường được đặt **sau tầng tính toán (linear/convolution)** và **trước hàm kích hoạt (activation function)**, mặc dù paper gốc đề xuất thứ tự ngược lại. Thứ tự này có thể thay đổi tùy vào thiết kế mạng cụ thể và kết quả thực nghiệm.\n\nMột điểm khác biệt quan trọng giữa **Batch Normalization** và các kỹ thuật khác như **Layer Normalization** là BN chuẩn hóa **trên từng đặc trưng trong batch**, trong khi LayerNorm chuẩn hóa **trên toàn bộ đặc trưng của từng mẫu riêng lẻ**.\n\n## 4\\. Batch Normalization Trong Pha Suy Diễn (Inference)\n\nTrong giai đoạn suy diễn (inference), mô hình chỉ nhận vào **một mẫu duy nhất**, nên việc sử dụng giá trị trung bình và phương sai tính từ batch là không khả thi. Do đó, trong quá trình huấn luyện, BN sẽ duy trì **trung bình động (moving average)** của mean và variance để sử dụng trong pha inference.\n\nCách làm này đảm bảo sự ổn định và nhất quán của mô hình khi đưa vào các dữ liệu mới sau khi đã huấn luyện xong.\n\n## 5\\. Một Số Ghi Chú và Ứng Dụng Trong CNN\n\nTrong mạng nơ-ron tích chập (CNN), Batch Normalization được áp dụng **riêng biệt cho từng kênh (channel)**, tức là mỗi channel sẽ được xem như một tập các đặc trưng độc lập, và việc chuẩn hóa được thực hiện riêng rẽ trên từng channel.\n\nKỹ thuật này đã trở thành một thành phần không thể thiếu trong các mạng học sâu hiện đại như ResNet, Inception, EfficientNet,… do khả năng cải thiện đáng kể tốc độ hội tụ và độ chính xác của mô hình.\n\n## 6\\. Kết Luận\n\nBatch Normalization là một trong những kỹ thuật chuẩn hóa mạnh mẽ và hiệu quả nhất trong huấn luyện mạng nơ-ron. Bằng việc giảm sự thay đổi trong phân phối đầu vào của các lớp, BN không chỉ giúp quá trình huấn luyện ổn định hơn mà còn nâng cao hiệu suất suy diễn của mô hình. Việc hiểu và sử dụng đúng Batch Normalization là một kỹ năng thiết yếu cho bất kỳ ai đang nghiên cứu hoặc làm việc trong lĩnh vực học sâu (deep learning).\n\n**Tài liệu tham khảo:**\n\n-   [https://leimao.github.io/blog/Batch-Normalization/\n    \n    ](https://leimao.github.io/blog/Batch-Normalization/)","tags":["Machine Learning","Deep Learning","Neural Network","Batch Normalization"]},{"slug":"resnet","title":"Sự Ra Đời Của ResNet: Giải Quyết Những Rào Cản Của Mạng Nơ-ron Sâu","date":"2025-04-25","topic":"paper","summary":"Sau thành công của AlexNet (2012), xu hướng chung của cộng đồng AI là thiết kế các mạng ngày càng sâu (như VGG, GoogLeNet) để tăng độ chính xác. Tuy nhiên, khi tăng độ sâu, các nhà nghiên cứu gặp phải hai vấn đề lớn:","body":"# 1\\. Sự ra đời của ResNet\n\nSau thành công của AlexNet (2012), xu hướng chung của cộng đồng AI là thiết kế các mạng ngày càng sâu (như VGG, GoogLeNet) để tăng độ chính xác. Tuy nhiên, khi tăng độ sâu, các nhà nghiên cứu gặp phải hai vấn đề lớn:\n\n## Vanishing Gradient\n\nKhi lan truyền ngược (backpropagation) qua nhiều lớp, gradient có nguy cơ bị triệt tiêu về gần 0, khiến các lớp đầu tiên không thể cập nhật trọng số và mạng bị “đóng băng”.\n\n## Degradation Problem\n\nĐây là nguyên nhân chính dẫn đến sự ra đời của ResNet. Khi mạng đủ sâu và bắt đầu hội tụ, việc chèn thêm các lớp không làm mạng tốt hơn mà lại khiến sai số huấn luyện và sai số kiểm tra tăng lên. Vấn đề này không phải do overfitting (vì training error cũng tăng), mà do các thuật toán tối ưu (như SGD) gặp khó khăn trong việc học một ánh xạ đồng nhất (identity mapping — tức là học cách “không làm gì cả”) qua nhiều lớp phi tuyến.\n\n---\n\n# 2\\. Giả thuyết của tác giả và ý tưởng cốt lõi của ResNet\n\nĐối mặt với Degradation Problem, Kaiming He và các cộng sự (Microsoft Research) đã đặt ra một giả thuyết:\n\n> “Nếu nhiều lớp phi tuyến có thể xấp xỉ các hàm phức tạp, thì chúng cũng hoàn toàn có thể xấp xỉ các hàm phần dư (residual functions).”\n\nThay vì để mạng cố gắng học trực tiếp một ánh xạ mục tiêu $H(x)$, ResNet yêu cầu mạng học **phần dư**:\n\n$F(x) := H(x) - x$\n\nTrong đó:\n\n-   $x$: Đầu vào (input)\n-   $H(x)$: Ánh xạ mục tiêu (hàm mục tiêu)\n-   $F(x)$: Phần dư (residual) — đại diện cho sự chênh lệch giữa cái đã biết và cái cần đạt tới\n\nSau đó, ta chỉ cần cộng lại đầu vào $x$ thông qua một đường tắt (shortcut connection), ta có công thức cơ bản:\n\n$y = F(x, \\{W_i\\}) + x$\n\nTrong đó:\n\n-   $F(x, \\{W_i\\})$: Hàm phần dư, được tính toán thông qua các lớp trọng số $\\{W_i\\}$\n\n## Tại sao lại như vậy?\n\nNếu mạng nhận thấy các lớp hiện tại đã đủ tốt, nó chỉ cần đẩy trọng số của hàm phần dư $F(x)$ về $0$. Việc đẩy $F(x) \\to 0$ dễ dàng hơn rất nhiều so với việc bắt một chuỗi các lớp phi tuyến phải tự học cách biến đầu vào thành chính nó (identity mapping).\n\n---\n\n# 3\\. Kiến trúc cơ bản của ResNet\n\nỞ phần này, bạn có thể theo dõi Figure 3 trong paper để hiểu rõ hơn.\n\n[Link paper gốc](https://arxiv.org/abs/1512.03385)\n\nKiến trúc cơ sở (Plain Network) của ResNet được lấy cảm hứng từ VGG Nets với hai quy tắc thiết kế:\n\n## Quy tắc 1: Giữ nguyên số bộ lọc khi feature map không đổi\n\n**Nếu kích thước feature map đầu ra không đổi, giữ nguyên số bộ lọc (filters).**\n\nKhi ảnh đi qua các lớp tích chập (convolutional layers) mà không bị giảm độ phân giải không gian, lượng thông tin không gian vẫn được giữ lại. Ở cùng một cấp độ thông tin này, mạng không cần tạo thêm các bộ lọc mới.\n\n**Ý nghĩa:** Giữ nguyên số bộ lọc giúp mạng tập trung trích xuất sâu hơn các đặc trưng ở độ phân giải đó mà không làm phình to số lượng tham số một cách không cần thiết.\n\n## Quy tắc 2: Nhân đôi số bộ lọc khi feature map giảm\n\n**Nếu kích thước feature map giảm đi một nửa (do dùng stride = 2), số bộ lọc phải nhân đôi** để bảo toàn độ phức tạp tính toán (time complexity) cho mỗi lớp.\n\n### Vấn đề\n\nTrong mạng CNN, để học được các đặc trưng tổng quát hơn (từ chi tiết nhỏ lẻ thành hình dáng lớn), người ta thường giảm kích thước ảnh đi một nửa (dùng pooling hoặc convolution với stride = 2). Khi đó, ảnh từ $56 \\times 56$ sẽ bị thu nhỏ thành $28 \\times 28$.\n\n### Toán học đằng sau\n\nKhối lượng tính toán (FLOPs) của một lớp tích chập phụ thuộc tỷ lệ thuận vào 2 yếu tố chính:\n\n1.  **Diện tích không gian (Spatial Area):** $H \\times W$\n2.  **Độ dày của kênh (Channel Depth):** $C_{in} \\times C_{out}$\n\nKhi ta giảm kích thước chiều dài và chiều rộng đi một nửa:\n\n-   **Diện tích mới:** $\\frac{H}{2} \\times \\frac{W}{2} = \\frac{H \\times W}{4}$\n-   Kích thước ảnh giảm một nửa đồng nghĩa với việc diện tích ảnh **giảm đi 4 lần**.\n\n### Giải pháp bù đắp\n\nĐể bù đắp sự sụt giảm 4 lần này và giữ khối lượng tính toán mỗi lớp luôn bằng nhau, tác giả **nhân đôi số lượng bộ lọc** (ví dụ từ 64 kênh lên 128 kênh).\n\n-   Khi nhân đôi số bộ lọc, $C_{out}$ tăng gấp 2. Đồng thời, $C_{in}$ của lớp tiếp theo cũng tăng gấp 2.\n-   Tích $(C_{in} \\times C_{out})$ trở thành $(2C_{in} \\times 2C_{out}) = 4 \\times (C_{in} \\times C_{out})$.\n-   **Kết quả:** Sự sụt giảm 4 lần về diện tích không gian được bù trừ hoàn hảo bởi sự tăng lên 4 lần do nhân đôi số kênh.\n\n## Shortcut Connections\n\nTừ kiến trúc cơ sở này, ResNet chèn thêm các kết nối tắt (shortcut connections) để tạo thành các khối Residual. Các đường tắt dạng identity **không bổ sung thêm bất kỳ tham số nào** và cũng **không làm tăng độ phức tạp tính toán**.\n\n## Residual Learning: A Building Block\n\n![Residual Block](/api/uploads/742eed9ec68242259796.png)\n\n**Hình 2:** Residual Learning — A Building Block\n\n---\n\n# 4\\. Giải quyết vấn đề chênh lệch số chiều (Dimension Mismatch)\n\nKhi feature map bị giảm kích thước (ví dụ từ $56 \\times 56$ xuống $28 \\times 28$) và số kênh tăng lên, shortcut $x$ không thể cộng trực tiếp với $F(x)$ do khác chiều. ResNet đưa ra 2 phương án chính:\n\n## Option A: Zero-Padding\n\nLấy mẫu phụ (subsampling) rồi đệm thêm các giá trị $0$ cho đủ số kênh.\n\n-   **Ưu điểm:** Không tốn thêm tham số tính toán.\n-   **Nhược điểm:** Các kênh đệm số 0 hoàn toàn không mang thông tin hay “đóng góp” vào việc học.\n\n## Option B: Projection Shortcut\n\nDùng một lớp tích chập $1 \\times 1$ (với stride = 2) để tạo phép chiếu tuyến tính $W_s$ nhằm khớp số chiều:\n\n$y = F(x, \\{W_i\\}) + W_s \\cdot x$\n\n-   **Ưu điểm:** Có khả năng học cách kết hợp các kênh cũ thành kênh mới chứa thông tin hữu ích hơn.\n-   **Nhược điểm:** Tốn thêm tham số cho mạng.\n\n**Thực nghiệm cho thấy** Option B nhỉnh hơn Option A một chút, nhưng Option A vẫn đủ để giải quyết Degradation Problem một cách tiết kiệm. Thông thường, Option B chỉ được khuyến nghị tại ranh giới giữa các stage (vị trí feature map thay đổi kích thước) để giảm thiểu tham số.\n\n---\n\n# 5\\. So sánh ResNet với VGG và Plain Network\n\nKhi đối chiếu:\n\n| Kiến trúc | FLOPs |\n| --- | --- |\n| VGG-19 | 19.6 tỷ |\n| 34-layer Plain | 3.6 tỷ |\n| 34-layer ResNet | 3.6 tỷ (bằng 18% VGG) |\n\n## Lý do ResNet ít tham số/FLOPs nhưng lại mạnh hơn\n\n### 1\\. Loại bỏ các lớp Fully Connected (FC) khổng lồ\n\nVGG sử dụng 3 lớp FC ở cuối mạng chứa lượng tham số khổng lồ, trong khi ResNet chỉ dùng:\n\n-   1 lớp Global Average Pooling\n-   1 lớp FC duy nhất\n\n### 2\\. Quy tắc bảo toàn Time Complexity\n\nViệc khống chế số lượng filter tăng tỷ lệ nghịch với độ phân giải không gian giúp lượng tính toán ổn định qua các lớp thay vì bùng nổ.\n\n### 3\\. Tối ưu không tốn phí\n\nShortcut connection chỉ thực hiện phép cộng ma trận (element-wise addition), nên mạng dù sâu đến 34 lớp vẫn tốn cùng mức FLOPs như mạng Plain 34 lớp, **nhưng cho lỗi huấn luyện thấp hơn hẳn**.\n\n---\n\n# 6\\. Kiến trúc Bottleneck ở các mạng siêu sâu (ResNet-50/101/152)\n\nKhi độ sâu mạng vượt qua 50 lớp, thời gian và bộ nhớ huấn luyện trở thành gánh nặng. Để giải quyết, tác giả đã thiết kế lại khối Residual thành dạng **Bottleneck (cổ chai)**.\n\n## Cấu trúc Bottleneck\n\nThay vì dùng 2 lớp Convolution $3 \\times 3$ liên tiếp, khối Bottleneck dùng 3 lớp:\n\n1.  **Lớp Conv $1 \\times 1$:** Nén (reduce) số chiều xuống.\n2.  **Lớp Conv $3 \\times 3$:** Trích xuất đặc trưng trên số chiều đã được làm nhỏ.\n3.  **Lớp Conv $1 \\times 1$:** Giãn (restore) số chiều trở lại ban đầu trước khi cộng với shortcut.\n\n## Tại sao nó giảm tham số?\n\nPhép toán Conv $3 \\times 3$ trực tiếp trên 256 kênh tốn rất nhiều tham số (ví dụ: $3 \\times 3 \\times 256 \\times 256$). Nhờ thiết kế cổ chai, ta:\n\n1.  Nén xuống còn 64 kênh\n2.  Thực hiện $3 \\times 3$ trên 64 kênh\n3.  Giãn lại ra 256 kênh\n\nĐiều này giúp **giảm số lượng tham số xuống khoảng 17 lần cho mỗi block**, biến những mạng như **ResNet-152 trở nên khả thi để huấn luyện** mà vẫn giữ được sức mạnh vượt trội.\n\n## So sánh: Basic Block vs Bottleneck Block\n\n![Residual Blocks Comparison](/api/uploads/6807644abc7dfdb95a49.png)\n\n**Hình 5:** A Deeper Residual Function for ImageNet\n\n-   **Trái:** Basic building block (56×56 feature maps) như trong ResNet-34\n-   **Phải:** Bottleneck building block cho ResNet-50/101/152\n\n---\n\n# Tóm tắt\n\n**ResNet** đã giải quyết vấn đề **Degradation Problem** thông qua:\n\n1.  ✅ **Residual Learning:** Học phần dư thay vì ánh xạ trực tiếp\n2.  ✅ **Shortcut Connections:** Kết nối tắt identity không tham số\n3.  ✅ **Kiến trúc thông minh:** Bảo toàn time complexity qua các lớp\n4.  ✅ **Bottleneck Design:** Cho phép xây dựng mạng siêu sâu hiệu quả\n\nNhờ những đổi mới này, ResNet không chỉ giải quyết vanishing gradient mà còn cho phép huấn luyện những mạng sâu hơn 100 lớp với hiệu suất vượt trội, mở ra một kỷ nguyên mới trong Deep Learning.","tags":["Deep Learning","Neural Network","Machine Learning","Computer Vision"]},{"slug":"python-cac-khai-niem-co-ban","title":"Các Khái Niệm Cơ Bản Trong Ngôn Ngữ Lập Trình Python","date":"2024-04-04","topic":"tutorial","summary":"Ngôn ngữ lập trình Python là ngôn ngữ kịch bản (scripting) đa năng, phổ biến trong nhiều lĩnh vực như khoa học dữ liệu, phát triển web, tự động hóa và trí tuệ nhân tạo. Bài viết này sẽ trình bày các khái niệm nền tảng: kiểu dữ liệu, biến, cấu trúc…","body":"# Giới thiệu\n\nNgôn ngữ lập trình **Python** là ngôn ngữ kịch bản (scripting) đa năng, phổ biến trong nhiều lĩnh vực như khoa học dữ liệu, phát triển web, tự động hóa và trí tuệ nhân tạo. Bài viết này sẽ trình bày các khái niệm nền tảng: kiểu dữ liệu, biến, cấu trúc điều khiển, toán tử, nhập/xuất dữ liệu và các lỗi phổ biến.\n\n# 1\\. Kiểu dữ liệu và biến\n\n-   **Kiểu số**: `int`, `float`\n-   **Chuỗi**: `str`\n-   **Boolean**: `bool` (`True`, `False`)\n-   **Cấu trúc**: `list`, `tuple`, `dict`, `set`\n-   **Cú pháp gán biến**:\n    \n    ```python\n    x = 5           # int\n    y = 3.14        # float\n    name = \"Alice\"  # str\n    ```\n    \n\n-   Python tự nhận dạng kiểu dữ liệu, không cần khai báo trước.\n\n# 2\\. Ghi chú trong mã (Comments)\n\n-   **Dòng đơn**: sử dụng `#`\n    \n    ```python\n    # Đây là dòng chú thích\n    ```\n    \n-   **Đa dòng**: sử dụng `''' ... '''` hoặc `\"\"\" ... \"\"\"`\n    \n    ```python\n    \"\"\"\n    Đây là chú thích\n    nhiều dòng\n    \"\"\"\n    ```\n    \n\n# 3\\. Toán tử trong Python\n\n-   **Số học**: `+`, `-`, `*`, `/`, `//`, `%`, `**`\n-   **So sánh**: `==`, `!=`, `<`, `<=`, `>`, `>=`\n-   **Logic**: `and`, `or`, `not`\n\n# 4\\. Nhập và xuất dữ liệu\n\n-   **Xuất dữ liệu**:\n    \n    ```python\n    print(\"Hello, Python!\", x)\n    ```\n    \n-   **Nhập dữ liệu**:\n    \n    ```python\n    name = input(\"Mời bạn nhập tên: \")\n    age = int(input(\"Tuổi của bạn là: \"))\n    ```\n    \n\n# 5\\. Cấu trúc điều khiển\n\n-   **Điều kiện**:\n    \n    ```python\n    if age >= 18:\n        print(\"Bạn đã đủ tuổi.\")\n    else:\n        print(\"Bạn chưa đủ tuổi.\")\n    ```\n    \n-   **Vòng lặp for**:\n    \n    ```python\n    for i in range(5):\n        print(i)\n    ```\n    \n-   **Vòng lặp while**:\n    \n    ```python\n    while x < 10:\n        x += 1\n    ```\n    \n-   **Các từ khóa điều khiển**: `break`, `continue`, `pass`\n    \n\n# 6\\. Xử lý lỗi cơ bản\n\n```python\ntry:\n    num = int(input(\"Nhập số nguyên: \"))\nexcept ValueError:\n    print(\"Giá trị nhập không phải số nguyên!\")\nfinally:\n    print(\"Kết thúc chương trình.\")\n```\n\nGiúp chương trình không bị đổ vỡ khi gặp lỗi nhập liệu.\n\n# 7\\. Lỗi thường gặp\n\n-   **IndentationError**: sai độ thụt lề\n-   **TypeError**: phép toán không hợp lệ giữa kiểu dữ liệu\n-   **NameError**: sử dụng biến chưa được khai báo\n-   **ValueError**: chuyển đổi kiểu dữ liệu không hợp lệ\n\n# 8\\. Kết luận\n\nNhững khái niệm này là nền tảng giúp bạn dễ dàng tiến sâu hơn vào Python và các ứng dụng nâng cao. Hi vọng bài viết sẽ là hành trang vững chắc trên con đường học mã hóa của bạn. Hẹn gặp lại trong các bài học tiếp theo!","tags":["Python","Lập trình","Cơ bản"]},{"slug":"danh-sach-lien-ket-don","title":"Danh sách liên kết đơn","date":"2022-03-31","topic":"tutorial","summary":"Danh sách liên kết đơn (Single Linked List) là một cấu trúc dữ liệu động, nó là một danh sách mà mỗi phần tử đều liên kết với phần tử đúng sau nó trong danh sách. Mỗi phần tử (được gọi là một node hay nút) trong danh sách liên kết đơn là một cấu…","body":"![Danh sach lien ket don](/api/uploads/c2dfdb8fd2a1962b916a.jpg)\n\n[Youtube: Danh sách liên kết đơn | Khiêm Lê\n\n](https://youtu.be/UokzPpBcnr0)\n\n# Danh sách liên kết đơn là gì?\n\nDanh sách liên kết đơn (Single Linked List) là một cấu trúc dữ liệu động, nó là một danh sách mà mỗi phần tử đều liên kết với phần tử đúng sau nó trong danh sách. Mỗi phần tử (được gọi là một node hay nút) trong danh sách liên kết đơn là một cấu trúc có hai thành phần:\n\n-   Thành phần dữ liệu: lưu thông tin về bản thân phần tử đó.\n-   Thành phần liên kết: lưu địa chỉ phần tử đứng sau trong danh sách, nếu phần tử đó là phần tử cuối cùng thì thành phần này bằng NULL.\n\n![Minh hoạ danh sách liên kết đơn](/api/uploads/32e35ce3233115f240b6.png)\n\nMinh họa danh sách liên kết đơn\n\n# Đặc điểm của danh sách liên kết đơn\n\nDo danh sách liên kết đơn là một cấu trúc dữ liệu động, được tạo nên nhờ việc cấp phát động nên nó có một số đặc điểm sau đây:\n\n-   Được cấp phát bộ nhớ khi chạy chương trình\n-   Có thể thay đổi kích thước qua việc thêm, xóa phần tử\n-   Kích thước tối đa phụ thuộc vào bộ nhớ khả dụng của RAM\n-   Các phần tử được lưu trữ ngẫu nhiên (không liên tiếp) trong RAM\n\nVà do tính liên kết của phần tử đầu và phần tử đứng sau nó trong danh sách liên kết đơn, nó có các đặc điểm sau:\n\n-   Chỉ cần nắm được phần tử đầu và cuối là có thể quản lý được danh sách\n-   Truy cập tới phần tử ngẫu nhiên phải duyệt từ đầu đến vị trí đó\n-   Chỉ có thể tìm kiếm tuyến tính một phần tử\n\n# Cài đặt danh sách liên kết đơn\n\nTrước khi đi vào cài đặt danh sách liên kết đơn, hãy chắc chắn rằng bạn đã nắm vững phần con trỏ và cấp phát động trong C++. Do danh sách liên kết đơn là một cấu trúc dữ liệu động, nếu bạn không nắm vững con trỏ và cấp phát động sẽ rất khó để bạn hiểu được bài viết này. Nếu bạn cảm thấy chưa tự tin, hãy dành ít thời gian để xem bài viết *Con trỏ và cấp phát động trong C++*[\\[1\\]](#tham-khao) của mình. Còn bây giờ thì bắt đầu thôi!\n\n## Tạo node\n\nDanh sách liên kết đơn được tạo thành từ nhiều node, do đó, chúng ta sẽ cùng đi từ node trước. Một node gồm hai thành phần là thành phần dữ liệu và thành phần liên kết. Thành phần dữ liệu có thể là kiểu dữ liệu có sẵn hoặc bạn tự định nghĩa (struct hay class…), trong bài viết này để đơn giản mình sẽ sử dụng kiểu int cho phần dữ liệu. Thành phần liên kết là địa chỉ đương nhiên sẽ là con trỏ, con trỏ này trỏ đến node tiếp theo, do đó, con trỏ này là con trỏ trỏ vào một node.\n\n```cpp\nstruct Node\n{\n\tint data;\n\tNode* next;\n};\n```\n\nĐể tạo một node mới, ta thực hiện cấp phát động cho node mới, khởi tạo giá trị ban đầu và trả về địa chỉ của node mới được cấp phát.\n\n```cpp\nNode* CreateNode(int init_data)\n{\n\tNode* node = new Node;\n\tnode->data = init_data;\n\tnode->next = NULL;      // node vừa tạo chưa thêm vào danh sách nên chưa liên kết với phần tử nào cả nên phần liên kết gán bằng NULL\n\treturn node;\n}\n```\n\n## Tạo danh sách liên kết đơn\n\nTa đã có được thành phần tạo nên danh sách liên kết đơn là node, tiếp theo chúng ta cần quản lý chúng bằng cách biết được phần tử đầu và cuối. Vì mỗi phần tử đều liên kết với phần tử kế vậy nên tả chỉ cần biết phần tử đầu và cuối là có thể quản lý được danh sách này. Vậy đơn giản ta cần tạo một cấu trúc lưu trữ địa chỉ phần tử đầu (head) và phần tử cuối (hay phần tử đuôi tail).\n\n```cpp\nstruct LinkedList\n{\n\tNode* head;\n\tNode* tail;\n};\n```\n\nKhi mới tạo danh sách, danh sách sẽ không có phần tử nào, do đó head và tail không trỏ vào đâu cả, ta sẽ gán chúng bằng NULL. Ta xây dựng hàm tạo danh sách như sau:\n\n```cpp\nvoid CreateList(LinkedList& l)\n{\n\tl.head = NULL;\n\tl.tail = NULL;\n}\n```\n\nBây giờ để tạo một danh sách, ta làm như sau:\n\n```cpp\nLinkedList list;\nCreateList(list); // Gán head và tail bằng NULL\n```\n\n## Thêm phần tử vào danh sách\n\n### Thêm vào đầu\n\nĐể thêm node vào đầu danh sách, đầu tiên ta cần kiếm tra xem danh sách đó có rỗng hay không, nếu danh sách rỗng, ta chỉ cần gán head và tail của danh sách bằng node đó. Ngược lại nếu danh sách không rỗng, ta thực hiện trỏ thành phần liên kết vào head, sau đó gán lại head bằng node mới.\n\n```cpp\nvoid AddHead(LinkedList& l, Node* node)\n{\n\tif (l.head == NULL)\n\t{\n\t\tl.head = node;\n\t\tl.tail = node;\n\t}\n\telse\n\t{\n\t\tnode->next = l.head;\n\t\tl.head = node;\n\t}\n}\n```\n\n![Thêm phần tử vào đầu danh sách liên kết đơn](/api/uploads/9dd3d7e458a06dde2839.png)\n\nThêm phần tử vào đầu danh sách liên kết đơn\n\nNhư trong hình trên, chúng ta thêm node có data bằng 0 vào danh sách. Ta thực hiện trỏ next của node đó vào head của danh sách (chính là node đầu tiên của danh sách có data bằng 1), sau đó ta trỏ head vào node có data 0 vừa được thêm. Vậy là phần tử đó đã nằm ở đầu danh sách rồi.\n\n### Thêm vào cuối\n\nTương tự, để thêm node vào cuối danh sách, đầu tiên ta kiểm tra xem danh sách rỗng hay không, rỗng thì gán head và tail đều bằng node mới. Nếu không rỗng, ta thực hiện trỏ tail→next vào node mới, sau đó gán lại tail bằng node mới (vì bây giờ node mới thêm chính là tail).\n\n```cpp\nvoid AddTail(LinkedList& l, Node* node)\n{\n\tif (l.head == NULL)\n\t{\n\t\tl.head = node;\n\t\tl.tail = node;\n\t}\n\telse\n\t{\n\t\tl.tail->next = node;\n\t\tl.tail = node;\n\t}\n}\n```\n\n![Thêm phần tử vào cuối danh sách liên kết đơn](/api/uploads/1666cbda6b55d5c460bc.png)\n\nThêm phần tử vào cuối danh sách liên kết đơn\n\nTrong hình trên, chúng ta thực hiện thêm node có data bằng 6 vào danh sách. Tail hiện tại là node có data 5, thực hiện gán tail→next bằng node mới để nối thêm nó vào đuôi danh sách, lúc này node mới trở thành phần tử cuối danh sách nên ta gán tail lại bằng node mới.\n\n### Thêm vào sau node bất kỳ\n\nĐể thêm một node p vào sau node q bất kỳ, đầu tiên ta cần kiếm tra xem node q có NULL hay không, nếu node q là NULL tức là danh sách rỗng, vậy thì ta sẽ thêm vào đầu danh sách. Nếu node q không NULL, tức là tồn tại trong danh sách, ta thực hiện trỏ p→next = q→next, sau đó q→next = p. Tiếp theo chúng ta kiểm tra xem node q trước đó có phải là node cuối hay không, nếu node q là node cuối thì thêm p vào, p sẽ thành node cuối nên ta gán lại tail = p.\n\n```cpp\nvoid InsertAfterQ(LinkedList& l, Node* p, Node* q)\n{\n\tif (q != NULL)\n\t{\n\t\tp->next = q->next;\n\t\tq->next = p;\n\t\tif (l.tail == q)\n\t\t\tl.tail = p;\n\t}\n\telse\n\t\tAddHead(l, p);\n}\n```\n\n![Thêm phần tử vào sau nút Q trong danh sách liên kết đơn](/api/uploads/3c29a36befe6baf88343.png)\n\nThêm phần tử vào sau nút Q trong danh sách liên kết đơn\n\nTrong hình trên, ta thêm node có data bằng 4 (node p) vào sau node có data bằng 3 (node q). Ta trỏ next của node p vào next của node q tức là node có data bằng 5, sau đó trỏ next của node q vào node p vậy là node p đã được thêm vào danh sách.\n\n## Xóa phần tử khỏi danh sách\n\n### Xóa ở đầu\n\nĐể xóa phần tử ở đầu danh sách, ta kiểm tra xem danh sách đó có rỗng hay không, nếu rỗng, ta không cần xóa, trả về kết quả là 0. Nếu danh sách không rỗng, ta thực hiện lưu node head lại, sau đó gán head bằng next của node head, sau đó xóa node head đi. Tiếp theo ta cần kiểm tra xem danh sách vừa bị xóa đi node head có rỗng hay không, nếu rỗng ta gán lại tail bằng NULL luôn sau đó trả về kết quả 1.\n\n```cpp\nint RemoveHead(LinkedList& l, int& x)\n{\n\tif (l.head != NULL)\n\t{\n\t\tNode* node = l.head;\n\t\tx = node->data;      // Lưu giá trị của node head lại\n\t\tl.head = node->next;\n\t\tdelete node;         // Hủy node head đi\n\t\tif (l.head == NULL)\n\t\t\tl.tail = NULL;\n\t\treturn 1;\n\t}\n\treturn 0;\n}\n```\n\nLưu ý trước khi xóa node head đi, ta dùng biến tham chiếu x để lưu trữ lại giá trị của node bị hủy để sử dụng.\n\n![Xóa phần tử đầu danh sách liên kết đơn](/api/uploads/4970d263c03ebd0947c4.png)\n\nXóa phần tử đầu danh sách liên kết đơn\n\nTrong hình trên, mình thực hiện xóa node đầu tiên có data bằng 0. Mình trỏ head đến next của node 0 (hiện đang là head), thì head lúc này sẽ là node 1, sau đó mình hủy đi node 0 là được.\n\n### Xóa ở sau node bất kỳ\n\nĐể xóa một node p sau node q bất kỳ, ta kiểm tra xem node q có NULL hay không, nếu node q NULL thì không tồn tại trong danh sách, do đó trả về 0, không xóa. Nếu node q khác NULL nhưng next của q là NULL, tức là p bằng NULL thì không xóa, trả về 0 (do sau q không có node nào cả, q là tail). Nếu node p tồn tại, ta thực hiện kiểm tra xem node p có phải là tail hay không, nếu node p là tail thì gán lại tail là q, tức là node trước đó để xóa node p đi.\n\n```cpp\nint RemoveAfterQ(LinkedList& l, Node* q, int& x)\n{\n\tif (q != NULL)\n\t{\n\t\tNode* p = q->next;\n\t\tif (p != NULL)\n\t\t{\n\t\t\tif (l.tail == p)\n\t\t\t\tl.tail = q;\n\t\t\tq->next = p->next;\n\t\t\tx = p->data;\n\t\t\tdelete p;\n\t\t\treturn 1;\n\t\t}\n\t\treturn 0;\n\t}\n\treturn 0;\n}\n```\n\n![Xóa phần tử sau nút Q trong danh sách liên kết đơn](/api/uploads/6e11978d184de5402c52.png)\n\nXóa phần tử sau nút Q trong danh sách liên kết đơn\n\nTrong hình trên, ta thực hiện xóa node có data 3 (node p) sau node có data 2 (node q). Ta trỏ next của node q vào next của node p tức là node có data 4, sau đó xóa node p đi là xong.\n\n## Duyệt danh sách và in\n\nSau khi có các thao tác thêm, xóa, chúng ta có thể in ra danh sách để kiểm tra xem có hoạt động đúng hay không. Để in danh sách, ta duyệt từ đầu đến cuối danh sách và in ra trong lúc duyệt. Ta gán một node bằng head, sau đó kiểm tra xem node đó có NULL hay không, không thì in ra data của node đó, sau đó gán tiếp node đó bằng next của chính nó tức node đó bây giờ là node tiếp theo, cứ như vậy cho đến hết.\n\n```cpp\nvoid PrintList(LinkedList l)\n{\n\tif (l.head != NULL)\n\t{\n\t\tNode* node = l.head;\n\t\twhile (node != NULL)\n\t\t{\n\t\t\tcout << node->data << ' ';\n\t\t\tnode = node->next; // Chuyển sang node tiếp theo\n\t\t}\n\t}\n}\n```\n\n## Lấy giá trị node bất kỳ\n\nĐể lấy giá trị phần tử trong danh sách, ta thực hiện duyệt tương tự như khi in phần tử. Ta sẽ tạo một biến đếm để biết vị trí hiện tại, duyệt qua các node cho đến khi node bằng NULL hoặc biến đếm bằng với vị trí node cần lấy. Kiểm tra xem nếu node khác NULL và biến đếm bằng vị trí cần lấy, ta sẽ trả về địa chỉ của node đó, ngược lại trả về NULL (danh sách rỗng hoặc là vị trí cần lấy nằm ngoài phạm vi của danh sách).\n\n```cpp\nNode* GetNode(LinkedList& l, int index)\n{\n\tNode* node = l.head;\n\tint i = 0;\n\twhile (node != NULL && i != index)\n\t{\n\t\tnode = node->next;\n\t\ti++;\n\t}\n\tif (i == index && node != NULL)\n\t\treturn node;\n\treturn NULL;\n}\n```\n\n## Tìm kiếm phần tử trong danh sách\n\nÝ tưởng tìm kiếm phần tử cũng là duyệt danh sách, nếu như chưa tìm thấy thì tiếp tục duyệt. Sau khi kết thúc duyệt, ta chỉ cần kiểm tra xem node duyệt có bằng NULL hay không, nếu không tức là đã tìm thấy, ta sẽ trả về địa chỉ của node đó.\n\n```cpp\nNode* Search(LinkedList l, int x)\n{\n\tNode* node = l.head;\n\twhile (node != NULL && node->data != x)\n\t\tnode = node->next;\n\tif (node != NULL)\n\t\treturn node;\n\treturn NULL;\n}\n```\n\n## Đếm số phần tử của danh sách\n\nĐếm số phần tử thì cũng tương tự, ta áp dụng duyệt từ đầu đếm cuối và đếm số node.\n\n```cpp\nint Length(LinkedList l)\n{\n\tint count = 0;\n\tNode* node = l.head;\n\twhile (node != NULL)\n\t{\n\t\tcount++;\n\t\tnode = node->next;\n\t}\n\treturn count;\n}\n```\n\n## Xóa danh sách\n\nĐể xóa danh sách, ta cần hủy tất cả các node tức là duyệt và hủy từng node. Ở đây mình sẽ dùng lại hàm RemoveHead. Đầu tiên, ta gán một node bằng head, kiểm tra nếu node đó khác NULL thì gọi RemoveHead và gán lại node bằng head tiếp, cứ lặp như vậy cho đến khi node đó NULL thì thôi. Sau khi xóa hết tất cả phần tử thì gán lại tail bằng NULL.\n\n```cpp\nvoid DestroyList(LinkedList& l)\n{\n\tint x;\n\tNode* node = l.head;\n\twhile (node != NULL)\n\t{\n\t\tRemoveHead(l, x);\n\t\tnode = l.head;\n\t}\n\tl.tail = NULL;\n}\n```\n\n# Tổng kết\n\nVậy là trong bài này, mình đã giới thiệu với các bạn về danh sách liên kết đơn và một số thao tác cơ bản trên danh sách. Các bạn không nhất thiết phải làm theo cách của mình, có rất nhiều cách để thực hiện khác nhau, chỉ cần bạn nắm vững về con trỏ và cấp phát động trong C++. Nếu thấy hay, đừng quên chia sẻ cho bạn bè. Cảm ơn các bạn đã theo dõi bài viết!\n\n# Source code\n\n```cpp\n#ifndef LinkedList_hpp\n#define LinkedList_hpp\n \nstruct Node\n{\n\tint data;\n\tNode* next;\n};\n \nstruct LinkedList\n{\n\tNode* head;\n\tNode* tail;\n};\n \nNode* CreateNode(int init_data);\nvoid CreateList(LinkedList& l);\nvoid AddHead(LinkedList& l, Node* node);\nvoid AddTail(LinkedList& l, Node* node);\nvoid InsertAfterQ(LinkedList& l, Node* p, Node* q);\nint RemoveHead(LinkedList& l, int& x);\nint RemoveTail(LinkedList& l, int& x);\nint RemoveAfterQ(LinkedList& l, Node* q, int& x);\nNode* GetNode(LinkedList l, int index);\nvoid PrintList(LinkedList l);\nNode* Search(LinkedList l, int x);\nint Length(LinkedList l);\nvoid DestroyList(LinkedList& l);\n \n#endif\n```\n\n```cpp\n#include <iostream>\n#include \"LinkedList.hpp\"\nusing namespace std;\n \nNode* CreateNode(int init_data)\n{\n\tNode* node = new Node;\n\tnode->data = init_data;\n\tnode->next = NULL;\n\treturn node;\n}\n \nvoid CreateList(LinkedList& l)\n{\n\tl.head = NULL;\n\tl.tail = NULL;\n}\n \nvoid AddHead(LinkedList& l, Node* node)\n{\n\tif (l.head == NULL)\n\t{\n\t\tl.head = node;\n\t\tl.tail = node;\n\t}\n\telse\n\t{\n\t\tnode->next = l.head;\n\t\tl.head = node;\n\t}\n}\n \nvoid AddTail(LinkedList& l, Node* node)\n{\n\tif (l.head == NULL)\n\t{\n\t\tl.head = node;\n\t\tl.tail = node;\n\t}\n\telse\n\t{\n\t\tl.tail->next = node;\n\t\tl.tail = node;\n\t}\n}\n \nvoid InsertAfterQ(LinkedList& l, Node* p, Node* q)\n{\n\tif (q != NULL)\n\t{\n\t\tp->next = q->next;\n\t\tq->next = p->next;\n\t\tif (l.tail == q)\n\t\t\tl.tail = p;\n\t}\n\telse\n\t\tAddHead(l, p);\n}\n \nint RemoveHead(LinkedList& l, int& x)\n{\n\tif (l.head != NULL)\n\t{\n\t\tNode* node = l.head;\n\t\tx = node->data;\n\t\tl.head = node->next;\n\t\tdelete node;\n\t\tif (l.head == NULL)\n\t\t\tl.tail = NULL;\n\t\treturn 1;\n\t}\n\treturn 0;\n}\n \nint RemoveAfterQ(LinkedList& l, Node* q, int& x)\n{\n\tif (q != NULL)\n\t{\n\t\tNode* p = q->next;\n\t\tif (p != NULL)\n\t\t{\n\t\t\tif (l.tail == p)\n\t\t\t\tl.tail = q;\n\t\t\tq->next = p->next;\n\t\t\tx = p->data;\n\t\t\tdelete p;\n\t\t\treturn 1;\n\t\t}\n\t\treturn 0;\n\t}\n\treturn 0;\n}\n \nNode* GetNode(LinkedList l, int index)\n{\n\tNode* node = l.head;\n\tint i = 0;\n\twhile (node != NULL && i != index)\n\t{\n\t\tnode = node->next;\n\t\ti++;\n\t}\n\tif (i == index && node != NULL)\n\t\treturn node;\n\treturn NULL;\n}\n \nvoid PrintList(LinkedList l)\n{\n\tif (l.head != NULL)\n\t{\n\t\tNode* node = l.head;\n\t\twhile (node != NULL)\n\t\t{\n\t\t\tcout << node->data << ' ';\n\t\t\tnode = node->next;\n\t\t}\n\t}\n}\n \nNode* Search(LinkedList l, int x)\n{\n\tNode* node = l.head;\n\twhile (node != NULL && node->data != x)\n\t\tnode = node->next;\n\tif (node != NULL)\n\t\treturn node;\n\treturn NULL;\n}\n \nint Length(LinkedList l)\n{\n\tint count = 0;\n\tNode* node = l.head;\n\twhile (node != NULL)\n\t{\n\t\tcount++;\n\t\tnode = node->next;\n\t}\n\treturn count;\n}\n \nvoid DestroyList(LinkedList& l)\n{\n\tint x;\n\tNode* node = l.head;\n\twhile (node != NULL)\n\t{\n\t\tRemoveHead(l, x);\n\t\tnode = l.head;\n\t}\n\tl.tail = NULL;\n}\n```\n\n```cpp\n#include <iostream>\n#include \"LinkedList.hpp\"\nusing namespace std;\n \nint main()\n{\n\t// Create a linked list\n\tLinkedList list;\n\tCreateList(list);\n \n\t// Add sample data to list\n\tNode* node;\n\tfor (auto i = 1; i <= 10; i++)\n\t{\n\t\t// Create new node with init data is i\n\t\tnode = CreateNode(i);\n \n\t\t// Add node to head\n\t\t// List that is added node by AddHead will be reversed\n\t\t//AddHead(list, node);\n \n\t\t// Add node to Tail\n\t\tAddTail(list, node);\n\t}\n \n\t// Print list\n\tPrintList(list);\n\tcout << endl;\n \n\t// Get list's length\n\tint len = Length(list);\n\tcout << \"Length of list: \" << len << endl;\n \n\t// Get node at index 7\n\tNode* nodeAtIdx7 = GetNode(list, 7);\n\tif (nodeAtIdx7 != NULL)\n\t\tcout << \"Data at node have idx 7: \" << nodeAtIdx7->data << endl;\n \n\t// Search for 4 in list\n\tNode* search4InList = Search(list, 4);\n\tif (search4InList != NULL)\n\t\tcout << \"4 was founded\" << endl;\n\telse\n\t\tcout << \"4 not Found\" << endl;\n \n\t// Remove node after 4 in list\n\tint x;\n\tint res = RemoveAfterQ(list, search4InList, x);\n\tif (res)\n\t{\n\t\tcout << \"Data of node has been removed: \" << x << endl;\n\t\tcout << \"List after removed: \";\n\t\tPrintList(list);\n\t\tcout << endl;\n\t}\n\telse\n\t\tcout << \"Nothing is removed\" << endl;\n \n\t// Insert 2409 after node 4\n\tNode* node2409 = CreateNode(2409);\n\tInsertAfterQ(list, node2409, search4InList);\n\tcout << \"List after insert 2409 after 4: \";\n\tPrintList(list);\n\tcout << endl;\n \n \n\t// Remove Head\n\tres = RemoveHead(list, x);\n\tif (res)\n\t{\n\t\tcout << \"Data of node has been removed: \" << x << endl;\n\t\tcout << \"List after removed head: \";\n\t\tPrintList(list);\n\t\tcout << endl;\n\t}\n\telse\n\t\tcout << \"Nothing is removed\" << endl;\n \n \n\t// Destroy all node\n\tDestroyList(list);\n \n\treturn 0;\n}\n```\n\n# Tham khảo\n\n1. [Con trỏ và cấp phát động trong C++](/blog/con-tro-va-cap-phat-dong-trong-cpp)","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"bang-bam-hash-table","title":"Bảng băm (Hash table)","date":"2020-06-22","topic":"tutorial","summary":"Bảng băm hay HashTable là một cấu trúc mà khi người dùng thực hiện truy xuất một phần tử qua khóa thì nó sẽ được ánh xạ vào thông qua hàm băm (Hash function).","body":"![Bảng băm trong C++](/api/uploads/87ae75aab1a36b10b447.jpg)\n\n# Bảng băm là gì?\n\nBảng băm hay HashTable là một cấu trúc mà khi người dùng thực hiện truy xuất một phần tử qua khóa thì nó sẽ được ánh xạ vào thông qua hàm băm (Hash function).\n\nQuá trình ánh xạ khóa vào bảng băm được thực hiện thông qua hàm băm (Hashing). Một bảng băm tốt cần phải có hàm băm tốt. Bảng băm là một mảng có M vị trí được đánh số từ 0 đến M - 1.\n\n![Bảng băm](/api/uploads/47084a37ecd91145a884.jpg)\n\nHashTable\n\nCó rất nhiều cách cài đặt kết nối của bảng băm như trực tiếp, dò tuyến tính, dò bậc hai, băm kép… Trong bài viết này mình sẽ giới thiệu đến các bạn phương pháp kết nối dò trực tiếp. Nhưng trước tiên, ta cần tìm hiểu hàm băm trước đã vì như mình đã nói, một bảng băm tốt khi nó có hàm băm tốt.\n\nTrước khi vào bài, phương pháp kết nối trực tiếp là phương pháp sử dụng danh sách liên kết đơn, do đó, bạn nào chưa biết gì về danh sách liên kết đơn thì hãy xem lại bài *Danh sách liên kết đơn*[\\[1\\]](#tham-khao) để hiểu rõ hơn nha.\n\n# Hàm băm\n\nHàm băm hay là Hash function là hàm thực hiện việc ánh xạ khóa k nào đó vào trong bảng băm (h(k)). Một hàm băm tốt thỏa mản các tiêu chí sau:\n\n-   Tốc độ tính toán nhanh\n-   Các khóa được phân bố đều trong bảng\n-   Ít xảy ra đụng độ\n\nMình sẽ giới thiệu đến các bạn các phép băm thường được sử dụng nhất là phương pháp chia và nhân.\n\nĐối với phương pháp chia, mình sẽ ánh xạ khóa theo hàm h(k) = k % M, với k là khóa và M là kích thước của bảng băm.\n\nĐối với phương pháp nhân, hàm ánh xạ h(k) = M \\* (k\\*A % 1), với k là khóa, M là kích thước bảng băm và A là số thực 0 < A < 1. Theo phương pháp nhân này, sự hiệu quả phụ thuộc vào việc lựa chọn A, theo như nhà khoa học máy tính Knuth, chọn A = (sqrt(5) - 1) / 2 là hiệu quả nhất (xấp xỉ 0.618033987).\n\nThông thường, mình sử dụng phương pháp chia cho dễ cài đặt. Tuy nhiên, không thể nào tránh khỏi đụng độ dù có dùng hàm băm nào đi nữa, do đó, chúng ta cần giải quyết đụng độ.\n\n# Giải quyết đụng độ\n\nĐối với việc sử dụng phương pháp kết nối trực tiếp, các phần tử bị đụng độ sẽ được thêm vào danh sách liên kết tại h(k) trong bảng băm.\n\n![Giải quyết đụng độ bảng băm](/api/uploads/a42c343c0cba6f669292.jpg)\n\nHashTable Chaining\n\nNhư bạn có thể thấy trong hình, các khóa như 7, 17 đụng độ nhau thì chúng sẽ được thêm vào danh sách liên kết ở h(k) = M. Tương tự các khóa 4, 19 cũng bị đụng và được thêm vào danh sách liên kết ở h(k) = 2…\n\nBây giờ chúng ta hãy cùng bắt đầu cài đặt bảng băm vào trong trong C++ nha.\n\n# Cấu trúc một nút trong bảng băm\n\nNhư đã nói, phương pháp kết nối trực tiếp dùng danh sách liên kết đơn, các phần tử bị đụng độ tại phần tử i trong bảng băm thì sẽ được thêm vào danh sách liên kết đơn tại i trong bảng băm. Do đó, một phần tử trong bảng băm có cấu trúc như một nút trong danh sách liên kết đơn.\n\n```cpp\nstruct Node\n{\n    int key;\n    Node* next;\n};\n```\n\n# Cấu trúc bảng băm và hàm khởi tạo\n\nMột bảng băm là một mảng chứa các nút, giả sử mình có 100 phần tử, vậy mình sẽ định nghĩa một HashTable như sau:\n\n```cpp\n#define M 100\n \ntypedef Node *HashTable[M];\n```\n\nNhư vậy, chúng ta có thể khai báo một bảng băm như sau:\n\n```cpp\nHashTable mHashTable;\n```\n\nCác bạn có thể dễ dàng thấy một nút trong bảng là một con trỏ trỏ đến một Node, như vậy, chúng ta cần phải khởi tạo chúng bằng NULL để tránh gặp lỗi. Mình sẽ có hàm khởi tạo bảng như sau:\n\n```cpp\nvoid InitHashTable(HashTable &HT)\n{\n    for (int i = 0; i < M; i++)\n        HT[i] = NULL;\n}\n```\n\n# Hàm băm\n\nNhư đã nói ở trên, để đơn giản mình sẽ sử dụng hàm băm theo phép chia:\n\n```cpp\nint Hash(int k)\n{\n    return k % M;\n}\n```\n\n# Thêm một nút vào bảng băm\n\nĐể thêm một nút, ta cần xác định vị trí sẽ thêm qua hàm băm h(k), sau đó thêm vào danh sách liên kết ở vị trí h(k) đó. Việc đụng độ sẽ được giải quyết do nếu đụng độ thì khóa sẽ được tự thêm vào sau danh sách liên kết đơn. Mình sẽ có hàm thêm như sau:\n\n```cpp\nvoid InsertNode(HashTable &HT, int k)\n{\n    int i = Hash(k);\n    AddTail(HT[i], k);\n}\n```\n\nHàm AddTail thì trong danh sách liên kết đơn, mình đã có bài viết về nó rồi, các bạn có thể đọc lại.\n\n```cpp\nvoid AddTail(Node *&l, int k)\n{\n    Node *newNode = new Node{k, NULL};\n    if (l == NULL)\n    {\n        l = newNode;\n    }\n    else\n    {\n        Node* p = l;\n        while (p != NULL && p->next != NULL)\n            p = p->next;\n        p->next = newNode;\n    }\n}\n```\n\n# Tìm kiếm một khóa trong bảng băm\n\nĐể tìm kiếm một khóa trong bảng băm, ta cũng thực hiện xác định vị trí h(k), sau đó ta thực hiện tìm kiếm trong danh sách liên kết tại vị trí h(k) trong bảng băm.\n\n```cpp\nNode *SearchNode(HashTable HT, int k)\n{\n    int i = Hash(k);\n    Node *p = HT[i];\n    while (p != NULL && p->key != k)\n        p = p->next;\n    if (p == NULL)\n        return NULL;\n    return p;\n}\n```\n\n# Xóa một nút ra khỏi bảng băm\n\nĐể xóa một phần tử ra khỏi bảng băm, đầu tiên ta cũng phải xác định h(k), sau đó tìm xem nó nằm ở đâu trong danh sách liên kết đơn tại vị trí h(k) đó rồi thực hiện xóa nó đi.\n\n```cpp\nvoid DeleteNode(HashTable &HT, int k)\n{\n    int i = Hash(k);\n    Node *p = HT[i];\n    Node *q = p;\n    while (p != NULL && p->key != k)\n    {\n        q = p; // Lưu lại địa chỉ của phần tử trước đó\n        p = p->next;\n    }\n    if (p == NULL)\n        cout << k << \" not found!\" << endl;\n    else if (p == HT[i])\n        DeleteHead(HT[i]); // Nút cần xóa là phần tử đầu của DSLK\n    else\n        DeleteAfter(q); // Xóa nút sau nút q\n}\n```\n\nHai hàm DeleteHead và DeleteAfter cũng đã được mình trình bày trong bài *Danh sách liên kết đơn*[\\[1\\]](#tham-khao) rồi nên mình sẽ không giả thích gì thêm.\n\n```cpp\nvoid DeleteHead(Node *&l)\n{\n    if (l != NULL)\n    {\n        Node *p = l;\n        l = l->next;\n        delete p;\n    }\n}\n \nvoid DeleteAfter(Node *&q)\n{\n    Node *p = q->next;\n    if (p != NULL)\n    {\n        q->next = p->next;\n        delete p;\n    }\n}\n```\n\n# Duyệt qua bảng băm\n\nDuyệt qua bảng băm rất đơn giản, bạn chỉ cần duyệt qua mảng, mỗi phần tử của mảng là một danh sách liên kết đơn, vậy thì duyệt danh sách liên kết đơn nữa là xong.\n\n```cpp\nvoid Traverse(Node *p) // duyệt DSLK\n{\n    while (p != NULL)\n    {\n        cout << p->key << ' ';\n        p = p->next;\n    }\n    cout << endl;\n}\n \nvoid TraverseHashTable(HashTable HT)\n{\n    for (int i = 0; i < M; i++)\n    {\n        cout << \"Bucket \" << i << \": \";\n        Traverse(HT[i]);\n    }\n}\n```\n\n# Lưu ý về bảng băm\n\nĐối với dữ liệu lớn, việc cấp phát một mảng quá lớn sẽ gây lãng phí bộ nhớ không đáng có, tuy nhiên, việc M lớn đảm bảo việc đụng độ ít xảy ra do các khóa phân bố đều. Ngược lại, nếu M nhỏ để tiết kiệm bộ nhớ, việc này sẽ làm giảm hiệu suất của bảng băm do việc đụng độ xảy ra với tần suất cao hơn.\n\nDo vậy, khi thao tác với bảng băm, các bạn cần phải cân nhắc giữa hiệu suất và dung lượng lưu trữ.\n\n# Tổng kết\n\nNhư vậy là trong bài viết này, mình đã giới thiệu đến các bạn về bảng băm trong C++, cách cài đặt bảng băm bằng phương thức kết nối trực tiếp dùng danh sách liên kết đơn. Nếu các bạn có bất kỳ ý kiến, đóng góp nào, đừng ngần ngại comment phía bên dưới bài viết nha. Cảm ơn các bạn đã theo dõi bài viết!\n\n# Source code\n\n```cpp\n#include <iostream>\nusing namespace std;\n \n#define M 10\n \nstruct Node\n{\n    int key;\n    Node *next;\n};\n \ntypedef Node *HashTable[M];\n \nvoid InitHashTable(HashTable &HT)\n{\n    for (int i = 0; i < M; i++)\n        HT[i] = NULL;\n}\n \nint Hash(int k)\n{\n    return k % M;\n}\n \nvoid AddTail(Node *&l, int k)\n{\n    Node *newNode = new Node{k, NULL};\n    if (l == NULL)\n    {\n        l = newNode;\n    }\n    else\n    {\n        Node* p = l;\n        while (p != NULL && p->next != NULL)\n            p = p->next;\n        p->next = newNode;\n    }\n}\n \nvoid InsertNode(HashTable &HT, int k)\n{\n    int i = Hash(k);\n    AddTail(HT[i], k);\n}\n \nvoid DeleteHead(Node *&l)\n{\n    if (l != NULL)\n    {\n        Node *p = l;\n        l = l->next;\n        delete p;\n    }\n}\n \nvoid DeleteAfter(Node *&q)\n{\n    Node *p = q->next;\n    if (p != NULL)\n    {\n        q->next = p->next;\n        delete p;\n    }\n}\n \nvoid DeleteNode(HashTable &HT, int k)\n{\n    int i = Hash(k);\n    Node *p = HT[i];\n    Node *q = p;\n    while (p != NULL && p->key != k)\n    {\n        q = p;\n        p = p->next;\n    }\n    if (p == NULL)\n        cout << k << \" not found!\" << endl;\n    else if (p == HT[i])\n        DeleteHead(HT[i]);\n    else\n        DeleteAfter(q);\n}\n \nNode *SearchNode(HashTable HT, int k)\n{\n    int i = Hash(k);\n    Node *p = HT[i];\n    while (p != NULL && p->key != k)\n        p = p->next;\n    if (p == NULL)\n        return NULL;\n    return p;\n}\n \nvoid Traverse(Node *p)\n{\n    while (p != NULL)\n    {\n        cout << p->key << ' ';\n        p = p->next;\n    }\n    cout << endl;\n}\n \nvoid TraverseHashTable(HashTable HT)\n{\n    for (int i = 0; i < M; i++)\n    {\n        cout << \"Bucket \" << i << \": \";\n        Traverse(HT[i]);\n    }\n}\n \nint main()\n{\n    HashTable mHashTable;\n    InitHashTable(mHashTable);\n \n    InsertNode(mHashTable, 0);\n    InsertNode(mHashTable, 1);\n    InsertNode(mHashTable, 2);\n    InsertNode(mHashTable, 3);\n    InsertNode(mHashTable, 10);\n    InsertNode(mHashTable, 13);\n    InsertNode(mHashTable, 9);\n    InsertNode(mHashTable, 11);\n \n    cout << \"HashTable:\\n\";\n    TraverseHashTable(mHashTable);\n \n    DeleteNode(mHashTable, 3);\n    DeleteNode(mHashTable, 13);\n    DeleteNode(mHashTable, 9);\n    cout << \"HashTable after Delete:\\n\";\n    TraverseHashTable(mHashTable);\n \n    Node *result = SearchNode(mHashTable, 10);\n    if (result == NULL)\n        cout << \"Not found!\";\n    else\n        cout << \"Found!\";\n \n    std::cout << std::endl;\n    system(\"pause\");\n    return 0;\n}\n```\n\n# Tham khảo\n\n1. [Danh sách liên kết đơn](/blog/danh-sach-lien-ket-don)","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"cay-nhi-phan","title":"Cây nhị phân và cây nhị phân tìm kiếm","date":"2020-06-10","topic":"tutorial","summary":"Cây nhị phân là một cấu trúc dữ liệu quan trọng mà trong môn Cấu trúc dữ liệu và giải thuật các bạn sẽ được học, nó được sử dụng rất rộng rãi trong lập trình vì các ứng dụng của nó. Trong bài viết này, mình sẽ giới thiệu đến các bạn về cây nhị phân…","body":"![Cây nhị phân](/api/uploads/0a321cde95cb12a61656.jpg)\n\n[Youtube: Cây nhị phân và cây nhị phân tìm kiếm | Khiêm Lê\n\n](https://youtu.be/hbqrBvflihI)\n\nCây nhị phân là một cấu trúc dữ liệu quan trọng mà trong môn Cấu trúc dữ liệu và giải thuật các bạn sẽ được học, nó được sử dụng rất rộng rãi trong lập trình vì các ứng dụng của nó. Trong bài viết này, mình sẽ giới thiệu đến các bạn về cây nhị phân và một phiên bản đặc biệt của nó là cây nhị phân tìm kiếm. Trước tiên, ta cần biết được cây là gì?\n\n# Cấu trúc cây\n\nCấu trúc cây (Tree) là một tập hợp các phần tử gọi là nút (node), mỗi cây có một nút gốc (root) chứa nhiều nút con, mỗi nút con lại là một tập hợp các nút khác gọi là cây con (subtree).\n\n![Cấu trúc cây](/api/uploads/c46bf051d905f82ccb2b.jpg)\n\nCấu trúc cây\n\nCác khái niệm cơ bản về cây:\n\n-   Bậc của nút: là số nút con của nút đó. Ví dụ bậc của nút A là 3, bậc của nút C là 1, bậc của nút G là 0…\n-   Bậc của cây: là bậc lớn nhất của nút trong cây đó, cây bậc n sẽ được gọi là cây n - phân. Ví dụ cây trong hình trên có bậc 3, gọi là cây tam phân, cây có bậc 2 gọi là cây nhị phân…\n-   Nút lá: nút lá là nút có bậc bằng 0. Ví dụ các nút lá: B, G, H, K, L, F\n-   Nút nhánh: là nút có bậc khác 0 mà không phải nút gốc (hay còn gọi là nút trung gian). Ví dụ các nút C, D, E\n-   Mức của nút: là số nguyên đếm từ 0, các nút ngang hàng nhau thì có cùng mức. Nút gốc A có mức là 0, mức 1 gồm các nút B, C, D, nút 3 gồm H, K, L.\n-   Chiều cao (chiều sâu): là mức lớn nhất của các nút lá. Ví dụ cây trên có nút lá bậc lớn nhất là H, K, L mức 3, vậy chiều cao của cây là 3.\n-   Độ dài đường đi đến nút x: là số nhánh (cạnh nối hai nút) cần đi qua tính từ nút gốc đến nút x. Hay độ dài đường đi đến nút mức i chính là i. Ví dụ nút E có độ dài đường đi là 2.\n\nKhi bạn đã nắm được các khái niệm cơ bản này, chúng ta hãy đến luôn với cây nhị phân.\n\n# Cây nhị phân\n\nCây nhị phân là một trường hợp đặc biệt của cấu trúc cây và nó cũng phổ biến nhất. Đúng như tên gọi của nó, cây nhị phân có bậc là 2 và mỗi nút trong cây nhị phân đều có bậc không quá 2.\n\n![Cây nhị phân](/api/uploads/c46bf051d905f82ccb2b.jpg)\n\nCây nhị phân\n\n## Các khái niệm\n\nCó một số khái niệm khác về cây nhị phân các bạn cần nắm như sau:\n\n-   Cây nhị phân đúng: là cây nhị phân mà mỗi nút của nó đều có bậc 2. Ví dụ như hình trên, hoặc hình trên bỏ đi nút H và I cũng là cây nhị phân đúng.\n-   Cây nhị phân đầy đủ là cây nhị phân có mức của các nút lá đều bằng nhau. Ví dụ hình trên, tất cả các nút lá đều có mức 3.\n-   Cây nhị phân tìm kiếm (sẽ tìm hiểu bên dưới)\n-   Cây nhị phân cân bằng: số phần tử của cây con bên trái chênh lệch không quá 1 so với cây con bên phải.\n\n## Định nghĩa cấu trúc nút\n\nNhìn vào hình, ta có thể dễ dàng phân tích được rằng, mỗi nút trong cây nhị phân sẽ gồm 3 thành phần như sau:\n\n-   Thành phần dữ liệu: có thể là bất kỳ kiểu dữ liệu nào.\n-   Thành phần liên kết trái: lưu trữ địa chỉ của nút gốc của cây con bên trái. Kiểu dữ liệu là con trỏ trỏ vào node.\n-   Thành phân liên kết phải: lưu trữ địa chỉ của nút gốc của cây con bên phải. Kiểu dữ liệu là con trỏ trỏ vào node.\n\nChúng ta sẽ có struct lưu trữ một node như sau - ở đây để đơn giản mình sử dụng kiểu dữ liệu int cho thành phần dữ liệu của node:\n\n```cpp\nstruct Node\n{\n    int data;\n    Node *left;\n    Node *right;\n};\n```\n\nKhi tạo một nút node mới, chúng ta cần phải gán lại các thành phần của node để nó không nhận giá trị rác, tránh lỗi không mong muốn. Chúng ta sẽ tạo một biến động cho node và trả về địa chỉ của node đó, mình sẽ có đoạn code tạo node như sau:\n\n```cpp\nNode *CreateNode(int init)\n{\n    Node *p = new Node;\n    p->data = init;\n    p->left = NULL;\n    p->right = NULL;\n    return p;\n}\n```\n\n## Định nghĩa cấu trúc cây\n\nĐể quản lý một cái cây, bạn chỉ cần quản lý được nút gốc, bạn có thể đi được đến các nhánh và lá của nó từ đó. Trên thực tế bạn không cần phải định nghĩa một kiểu dữ liệu nào để quản lý cả, tuy nhiên, để cho code rõ ràng hơn, bạn nên định nghĩa một kiểu dữ liệu cây nữa.\n\n```cpp\ntypedef Node* Tree;\n```\n\nLúc này, khi tạo một cây, bản chất là nó sẽ tạo cho bạn một con trỏ có thể trỏ vào một node.\n\n```cpp\nTree myTree;\n```\n\nVì nó là con trỏ nên các bạn gán nó bằng NULL để tránh lỗi, nhưng để mọi thứ rõ ràng hơn, mình sẽ dùng hàm tạo cây đơn giản gán nó bằng NULL.\n\n```cpp\nvoid CreateTree(Tree &root)\n{\n    root = NULL;\n}\n \n// Khi tạo cây\nCreateTree(myTree);\n```\n\n## Duyệt cây nhị phân\n\nCó 3 cách duyệt cây nhị phân:\n\n-   Duyệt tiền tự (NLR): duyệt nút gốc, duyệt tiền tự cây con trái, duyệt tiền tự cây con phải.\n-   Duyệt trung tự (LNR): duyệt trung tự cây con trái, duyệt nút gốc, duyệt trung tự cây con phải.\n-   Duyệt hậu tự (LRN): duyệt hậu tự cây con trái, duyệt hậu tự cây con phải, duyệt nút gốc.\n\nĐể bạn hiểu rõ hơn ba cách duyệt này, chúng ta sẽ sử dụng lại hình ảnh cây nhị phân trên:\n\n![Cây nhị phân](/api/uploads/b9906a38d5067368307f.jpg)\n\nCây nhị phân\n\n-   Duyệt tiền tự: A B D H I E K L C F M N G O P\n-   Duyệt trung tự: H D I B K E L A M F N C O G P\n-   Duyệt hậu tự: H I D K L E B M N F O P G C A\n\nỨng với từng cách duyệt đó, chúng ta sẽ có các hàm duyệt cây như sau:\n\n```cpp\nvoid NLR(Tree root)\n{\n    if (root)\n    {\n        // Xử lý nút gốc (root)\n        NLR(root->left);\n        NLR(root->right);\n    }\n}\n```\n\n```cpp\nvoid LNR(Tree root)\n{\n    if (root)\n    {\n        LNR(root->left);\n        // Xử lý nút gốc (root)\n        LNR(root->right);\n    }\n}\n```\n\n```cpp\nvoid LRN(Tree root)\n{\n    if (root)\n    {\n        LRN(root->left);\n        LRN(root->right);\n        // Xử lý nút gốc (root)\n    }\n}\n```\n\n## Hủy cây nhị phân\n\nĐể hủy đi cây nhị phân, các bạn cũng thực hiện duyệt và xóa đi các nút của cây, tuy nhiên, các bạn dễ thấy rằng, nếu ta duyệt tiền tự và trung tự, khi xóa nút nhánh thì sẽ bị mất luôn địa chỉ của các nút con. Do đó, việc hủy cây nhị phân bắt buộc phải duyệt hậu tự. Hay nói cách khác, bạn phải xóa các phần tử là nút lá xóa dần lên đến nút gốc.\n\nChúng ta sẽ có hàm hủy như sau:\n\n```cpp\nvoid DestroyTree(Tree &root)\n{\n    if (root)\n    {\n        DestroyTree(root->left);\n        DestroyTree(root->right);\n        delete root;\n    }\n}\n```\n\nNhư vậy là chúng ta đã tìm hiểu về cách tạo một nút, kết nối chúng lại thành một cây nhị phân, duyệt cây và hủy cây. Tiếp theo chúng ta sẽ tìm hiểu về cây nhị phân đặc biệt khác là cây nhị phân tìm kiếm.\n\n# Cây nhị phân tìm kiếm\n\nCây nhị phân tìm kiếm là cây nhị phân mà trong đó, các phần tử của cây con bên trái đều nhỏ hơn phần tử hiện hành và các phần tử của cây con bên phải đều lớn hơn phần tử hiện hành. Do tính chất này, cây nhị phân tìm kiếm không được có phần tử cùng giá trị.\n\n![Cây nhị phân tìm kiếm](/api/uploads/07c3c05c9def2c3ac365.jpg)\n\nBinary Search Tree\n\nNhờ vào tính chất đặc biệt này, cây nhị phân tìm kiếm được sử dụng để tìm kiếm phần tử nhanh hơn (tương tự với tìm kiếm nhị phân). Khi duyệt cây nhị phân theo cách duyệt trung tự, bạn sẽ thu được một mảng có thứ tự. Chúng ta sẽ lần lượt tìm hiểu qua chúng.\n\n## Thêm phần tử vào cây nhị phân tìm kiếm\n\nĐể thêm phần tử vào cây nhị phân tìm kiếm, ta phải thêm vào cây nhưng vẫn đảm bảo được cây đó vẫn là cây nhị phân tìm kiếm. Ví dụ thêm phần tử 12 vào cây trong hình trên, mình sẽ cần chèn vào vị trí bên trái 13. Hàm duyệt tìm vị trí thích hợp và chèn của mình như sau:\n\n```cpp\nvoid AddNode(Tree &root, Node *node)\n{\n    if (root)\n    {\n        if (root->data == node->data) // Nếu bị trùng giá trị thì không thêm\n            return;\n        if (node->data < root->data) // Thêm vào cây con bên trái (nhỏ hơn nút hiện tại)\n            AddNode(root->left, node);\n        else\n            AddNode(root->right, node); // Thêm vào cây con bên phải (lớn hơn nút hiện tại)\n    }\n    else\n    {\n        root = node; // Đã tìm thấy vị trí thích hợp, thêm node vào\n    }\n}\n```\n\n## Tìm một phần tử trong cây nhị phân tìm kiếm\n\nNhư đã giới thiệu ở trên, để tìm một phần tử trong cây nhị phân tìm kiếm, chúng ta sẽ thực hiện tương tự việc tìm kiếm nhị phân. Nếu như nút cần tìm nhỏ hơn nút đang xét, chúng ta sẽ tìm cây con bên trái, ngược lại chúng ta sẽ tìm trong cây con bên phải, nếu đúng nút cần tìm thì mình sẽ trả về địa chỉ của nút đó. Mình sẽ có thuật toán sau:\n\n```cpp\nNode *FindNode(Tree root, int x)\n{\n    if (root)\n    {\n        if (root->data == x) // Tìm thấy\n            return root;\n        if (x < root->data)\n            return FindNode(root->left, x); // Tìm cây con bên trái\n        return FindNode(root->right, x); // Tìm cây con bên phải\n    }\n    return NULL; // Không tìm thấy\n}\n```\n\n## Hủy nút trên cây nhị phân tìm kiếm\n\nĐể hủy một nút có khóa X trong cây nhị phân tìm kiếm, chúng ta cần giải quyết ba trường hợp sau:\n\n1.  Nút X là nút lá, ta xóa đi mà không làm ảnh hưởng đến các nút khác. Ví dụ xóa nút 15 đi không ảnh hưởng gì đến các nút khác.\n2.  Nút X có 1 cây con, chúng ta chỉ cần nối nút cha của X với nút con của X. Ví dụ xóa nút 13 đi, ta chỉ cần nối nút 18 và 15 lại, sau đó xóa nút 13 đi.\n3.  Nút X có đầy đủ 2 cây con: vì X có đầy đủ 2 nút nên nếu ta xóa đi, ta sẽ bị mất toàn bộ cây con. Do đó chúng ta cần tìm phần tử thế mạng cho X mà vẫn đảm bảo được cây nhị phân tìm kiếm, sau đó mới xóa X đi.\n\nĐối với hai trường hợp đầu thì dễ, tuy nhiên, với trường hợp thứ 3, chúng ta cần phải giải quyết vấn đề tìm phần tử thế mạng cho x, chúng ta sẽ có hai cách thực hiện như sau:\n\n1.  Nút thế mạng là nút có khóa nhỏ nhất (trái nhất) của cây con bên phải x.\n2.  Nút thế mạng là nút có khóa lớn nhất (phải nhất) của cây con bên trái x.\n\nLấy ví dụ cho các bạn dễ hiểu hơn, hình phía trên, xóa đi phần tử 18 theo cách 1, phần tử lớn nhất của cây con bên trái là 15, vậy thì thay 18 bằng 15 rồi xóa đi nút 15 cuối. Cách 2, phần tử nhỏ nhất của cây con bên phải là 23, vậy 18 sẽ thay bằng 23 và xóa nút 23 đó đi.\n\nĐối với hai trường hợp đầu tiên khá đơn giản, nên mình sẽ lồng nó vào code luôn ở phần dưới, mình sẽ giải quyết cách tìm phần tử thế mạng ở trường hợp 3 trước và theo cả hai cách. Theo cách 1, mình sẽ làm như sau:\n\n```cpp\n// nút p là nút cần thay thế, tree là cây đang xét (cây bên phải)\nvoid FindAndReplace1(Tree &p, Tree &tree)\n{\n    if (tree->left) // chưa phải nhỏ nhất (trái nhất)\n        FindAndReplace1(p, tree->left); // tiếp tục tìm\n    else // tree là nút trái nhất\n    {\n        p->data = tree->data; // copy data\n        p = tree; // trỏ nút p vào nút tree sẽ làm thế mạng bị xóa\n        tree = tree->right; // nút trái không còn tuy nhiên nút phải có thể còn nên ta phải nối chúng lại\n    }\n}\n```\n\nĐối với trường hợp này, các bạn phải gọi hàm FindAndReplace1(p, root→right) trong hàm DeleteNode ở phía trên. Trường hợp thứ 2 thì ngược lại.\n\n```cpp\n// nút p là nút cần thay thế, tree là cây đang xét (cây bên trái)\nvoid FindAndReplace2(Tree &p, Tree &tree)\n{\n    if (tree->right) // chưa phải lớn nhất (phải nhất)\n        FindAndReplace2(p, tree->right); // tiếp tục tìm\n    else // tree là nút trái nhất\n    {\n        p->data = tree->data; // copy data\n        p = tree; // trỏ nút p vào nút tree sẽ làm thế mạng bị xóa\n        tree = tree->left; // nút phải không còn tuy nhiên nút trái có thể còn nên ta phải nối chúng lại\n    }\n}\n```\n\nVà trong hàm DeleteNode, các bạn sẽ gọi hàm FindAndReplace(p, root→left). Bây giờ, tổng hợp lại, chúng ta đã có thể dể dàng xóa một nút khỏi cây nhị phân tìm kiếm, mình sẽ code như sau:\n\n```cpp\nvoid DeleteNode(Tree &root, int x)\n{\n    if (root)\n    {\n        if (x > root->data)\n            DeleteNode(root->right, x);\n        else if (x < root->data)\n            DeleteNode(root->left, x);\n        else // nút hiện tại (root) là nút cần xóa\n        {\n            Node *p = root; // lưu lại nút cần xóa tránh bị ghi đè\n            if (!root->left)\n                root = root->right; // trường hợp 1\n            else if (!root->right)\n                root = root->left; // trường hợp 2\n            else\n                FindAndReplace1(p, root->right); // cách 1\n                // FindAndReplace2(p, root->left); // cách 2\n            delete p; // xóa nút\n        }\n    }\n    else\n    {\n        cout << \"Not found!\\n\"; // Không tìm thấy phần tử cần xóa\n    }\n}\n```\n\n# Tổng kết\n\nVậy là qua bài viết này, mình đã giới thiệu đến các bạn về cấu trúc cây, cây nhị phân và cây nhị phân tìm kiếm. Đương nhiên, mình không phải “master” và mình cũng không thể nào mà nắm được hết tất cả các lý thuyết đồ thị hay thuật toán, do đó sai sót là không thể tránh khỏi, hy vọng các bạn góp ý thêm.\n\nCảm ơn các bạn đã theo dõi bài viết, nếu thấy bài viết này hay, đừng quên chia sẻ cho mọi người cùng biết nhé! Cảm ơn các bạn!\n\n[Luyện nghe tiếng anh dictation\n\n](https://engdictation.app/)\n\n# Source code\n\n```cpp\nstruct Node\n{\n    int data;\n    Node *left;\n    Node *right;\n};\n \ntypedef Node *Tree;\n \nNode *CreateNode(int init)\n{\n    Node *p = new Node;\n    p->data = init;\n    p->left = NULL;\n    p->right = NULL;\n    return p;\n}\n \nvoid CreateTree(Tree &root)\n{\n    root = NULL;\n}\n \nvoid DestroyTree(Tree &root)\n{\n    if (root)\n    {\n        DestroyTree(root->left);\n        DestroyTree(root->right);\n        delete root;\n    }\n}\n \nvoid AddNode(Tree &root, Node *node)\n{\n    if (root)\n    {\n        if (root->data == node->data)\n            return;\n        if (node->data < root->data)\n            AddNode(root->left, node);\n        else\n            AddNode(root->right, node);\n    }\n    else\n    {\n        root = node;\n    }\n}\n \nNode *FindNode(Tree root, int x)\n{\n    if (root)\n    {\n        if (root->data == x)\n            return root;\n        if (x < root->data)\n            return FindNode(root->left, x);\n        return FindNode(root->right, x);\n    }\n    return NULL;\n}\n \nvoid PrintTree(Tree root)// print tree using LNR\n{\n    if (root)\n    {\n        PrintTree(root->left);\n        cout << root->data << ' ';\n        PrintTree(root->right);\n    }\n}\n \nvoid NLR(Tree root)\n{\n    if (root)\n    {\n        // Xử lý nút gốc (root)\n        NLR(root->left);\n        NLR(root->right);\n    }\n}\n \nvoid LNR(Tree root)\n{\n    if (root)\n    {\n        LNR(root->left);\n        // Xử lý nút gốc (root)\n        LNR(root->right);\n    }\n}\n \nvoid LRN(Tree root)\n{\n    if (root)\n    {\n        LRN(root->left);\n        LRN(root->right);\n        // Xử lý nút gốc (root)\n    }\n}\n \nvoid FindAndReplace1(Tree &p, Tree &tree)\n{\n    if (tree->left)\n        FindAndReplace1(p, tree->left);\n    else\n    {\n        p->data = tree->data;\n        p = tree;\n        tree = tree->right;\n    }\n}\n \nvoid FindAndReplace2(Tree &p, Tree &tree)\n{\n    if (tree->right)\n        FindAndReplace2(p, tree->right);\n    else\n    {\n        p->data = tree->data;\n        p = tree;\n        tree = tree->left;\n    }\n}\n \nvoid DeleteNode(Tree &root, int x)\n{\n    if (root)\n    {\n        if (x > root->data)\n            DeleteNode(root->right, x);\n        else if (x < root->data)\n            DeleteNode(root->left, x);\n        else\n        {\n            Node *p = root;\n            if (!root->left)\n                root = root->right;\n            else if (!root->right)\n                root = root->left;\n            else\n                FindAndReplace1(p, root->right);\n            // FindAndReplace2(p, root->left);\n            delete p;\n        }\n    }\n    else\n    {\n        cout << \"Not found!\\n\";\n    }\n}\n```","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"da-hinh-trong-cpp","title":"Đa hình trong C++","date":"2020-06-01","topic":"tutorial","summary":"Đa hình là một trong bốn tính chất đặc trưng của lập trình hướng đối tượng bên cạnh tính đóng gói, tính trừu tượng và tính kế thừa. Vậy thì đa hình là gì?","body":"![Đa hình trong C++](/api/uploads/b33dd85f5c82ce3d98a4.jpg)\n\n# Đa hình là gì?\n\nĐa hình là một trong bốn tính chất đặc trưng của lập trình hướng đối tượng bên cạnh tính đóng gói, tính trừu tượng và tính kế thừa. Vậy thì đa hình là gì?\n\nĐa hình (polymorphism) là hiện tượng mà các đối tượng thuộc các class khác nhau có thể biểu diễn cùng một thông thiệp theo các cách khác nhau. Hơi nặng về lý thuyết một chút nhưng xem ví dụ sau bạn sẽ rõ ngay!\n\nVí dụ hai con vật là con chó và con mèo, hai con vật này đều có thể phát ra tiếng nhưng con mèo sẽ kêu “meo meo” còn con chó lại sủa “gâu gâu”. Hành động phát ra tiếng này tuy là một hành động nhưng khi được 2 đối tượng khác nhau là chó và mèo thực hiện thì lại khác nhau.\n\nTrước khi đi vào chi tiết, hãy chắc chắn rằng bạn đã nắm được các kiến thức sau:\n\n-   *Con trỏ và cấp phát động trong C++*[\\[1\\]](#tham-khao)\n-   *Cơ bản về Class trong C++*[\\[2\\]](#tham-khao)\n-   *Kế thừa trong C++*[\\[3\\]](#tham-khao)\n-   *Vector trong C++*[\\[4\\]](#tham-khao)\n\n# Các loại đa hình\n\nCó hai loại đa hình, một là Compile time Polymorphism và Runtime Polymorphism. Compile time Polymorphism nghĩa là cách mà đối tượng thực hiện thông điệp được xác định ngay lúc biên dịch chương trình. Ví dụ cơ bản của Compile time Polymorphism là Overriding và Overloading.\n\nKhi bạn overloading, các hàm đó sẽ có cùng tên nhưng khác tham số, đó chính là đa hình, cùng là một hàm nhưng với các đối tượng là các tham số khác nhau thì hành động thực hiện cũng sẽ khác nhau. Tương tự với overriding, phương thức ở class cơ sở bị override ở class dẫn xuất, điều đó làm cho tuy cùng một phương thức nhưng đối tượng thuộc class cơ sở sẽ thực hiện khác đối tượng thuộc class dẫn xuất.\n\nĐối với Runtime Polymorphism, cách mà đối tượng thực hiện thông điệp không được xác định lúc biên dịch mà nó chỉ được xác định khi chương trình được thực thi. Sử dụng lại ví dụ ban đầu, vẫn là hành động phát ra tiếng của chó và mèo, tuy nhiên, bạn sẽ không thể biết thực hiện như thế nào cho đến khi biết đối tượng là con chó hay mèo để thực hiện kêu “meo meo” hay là sủa “gâu gâu”.\n\nCompile time Polymorphism thì chắc các bạn cũng đã quá quen thuộc rồi, trong bài viết này, mình sẽ chỉ giới thiệu Runtime Polymorphism thôi.\n\n# Lợi ích của đa hình\n\nNhư định nghĩa của đa hình, đa hình thực hiện cùng một thông điệp theo các cách khác nhau, do đó, code của chúng ta sẽ không bị lặp.\n\nVí dụ mình có bài toán công ty quản lý hai loại nhân viên là nhân viên văn phòng (NVVP) và nhân viên sản xuất (NVSX), mình sẽ cho người dùng nhập nhân nhiên và tính lương sau đó xuất ra. Mình sẽ có sơ đồ class như sau:\n\n![Sơ đồ class](/api/uploads/9cbdaf2d44e7bfec7abd.jpg)\n\nSơ đồ Class\n\nDựa trên sơ đồ đã xây dựng, mình sẽ code các class như sau:\n\n```cpp\nclass NhanVien\n{\nprotected:\n    string hoTen;\n    float luong;\n \npublic:\n    NhanVien()\n    {\n        this->hoTen = \"\";\n        this->luong = 0.0;\n    }\n \n    void nhap()\n    {\n        cout << \"Ho ten: \";\n        cin.ignore();\n        getline(cin, this->hoTen);\n    }\n \n    void xuat()\n    {\n        cout << \"Ho ten: \";\n        cout << this->hoTen << endl;\n    }\n \n    void tinhLuong() {}\n};\n```\n\n```cpp\nclass NhanVienSanXuat : public NhanVien\n{\nprivate:\n    int soSanPham;\n    float tienCong1SP;\n \npublic:\n    NhanVienSanXuat() : NhanVien()\n    {\n        this->soSanPham = 0;\n        this->tienCong1SP = 0;\n    }\n \n    void nhap()\n    {\n        NhanVien::nhap();\n        cout << \"So san pham: \";\n        cin >> this->soSanPham;\n        cout << \"Tien cong 1 san pham: \";\n        cin >> this->tienCong1SP;\n    }\n \n    void xuat()\n    {\n        cout << \"So san pham: \";\n        cout << this->soSanPham << endl;\n        cout << \"Tien cong 1 san pham: \";\n        cout << this->tienCong1SP << endl;\n        cout << \"Luong: \";\n        cout << this->luong << endl;\n    }\n \n    void tinhLuong()\n    {\n        this->luong = this->soSanPham * this->tienCong1SP;\n    }\n};\n```\n\n```cpp\nclass NhanVienVanPhong : public NhanVien\n{\nprivate:\n    float luongCoBan;\n    int soNgayLamViec;\n \npublic:\n    NhanVienVanPhong() : NhanVien()\n    {\n        this->luongCoBan = 0.0;\n    }\n \n    void nhap()\n    {\n        NhanVien::nhap();\n        cout << \"Luong co ban: \";\n        cin >> this->luongCoBan;\n        cout << \"So ngay lam viec: \";\n        cin >> this->soNgayLamViec;\n    }\n \n    void xuat()\n    {\n        NhanVien::xuat();\n        cout << \"Luong co ban: \";\n        cout << this->luongCoBan << endl;\n        cout << \"So ngay lam viec: \";\n        cout << this->soNgayLamViec << endl;\n        cout << \"Luong: \";\n        cout << this->luong << endl;\n    }\n \n    void tinhLuong()\n    {\n        this->luong = this->soNgayLamViec * this->luongCoBan;\n    }\n};\n```\n\n```cpp\nclass CongTy\n{\nprivate:\n    vector<NhanVienVanPhong> NVVP;\n    vector<NhanVienSanXuat> NVSX;\n \npublic:\n    void nhap()\n    {\n        cout << \"Nhap so nhan vien van phong: \";\n        int n;\n        cin >> n;\n        for (int i = 0; i < n; i++)\n        {\n            NhanVienVanPhong nv;\n            nv.nhap();\n            this->NVVP.push_back(nv);\n        }\n \n        cout << \"Nhap so nhan vien san xuat: \";\n        int m;\n        cin >> m;\n        for (int i = 0; i < m; i++)\n        {\n            NhanVienSanXuat nv;\n            nv.nhap();\n            this->NVSX.push_back(nv);\n        }\n    }\n \n    void xuat()\n    {\n        cout << \"Nhan vien van phong:\" << endl;\n        for (int i = 0; i < this->NVVP.size(); i++)\n        {\n            cout << \"STT:\" << i + 1 << endl;\n            this->NVVP.at(i).xuat();\n        }\n \n        cout << \"Nhan vien san xuat:\" << endl;\n        for (int i = 0; i < this->NVSX.size(); i++)\n        {\n            cout << \"STT:\" << i + 1 << endl;\n            this->NVSX.at(i).xuat();\n        }\n    }\n \n    void tinhLuong()\n    {\n        for (int i = 0; i < this->NVVP.size(); i++)\n            this->NVVP.at(i).tinhLuong();\n        for (int i = 0; i < this->NVSX.size(); i++)\n            this->NVSX.at(i).tinhLuong();\n    }\n};\n```\n\nNhìn vào những đoạn code trên, bạn có thể dễ dàng nhận ra rằng, tuy đều là hành động nhập, xuất và tính lương, tuy nhiên chúng ta lại phải gọi phương thức này cho 2 đối tượng thuộc kiểu khác nhau là NVVP và NVSX. Còn một điều nữa là tuy đều là nhân viên nhưng lại phải dùng 2 vector khác nhau để lưu trữ 2 đối tượng này.\n\nCâu hỏi đặt ra là liệu có cách nào để có thể lưu trữ 2 loại nhân viên này trong cùng một vector? Và phương thức nhập, xuất, tính lương có thể tự động biết nó thực hiện hành động cho đối tượng nào để thực hiện cho đúng không? Câu trả lời là có và nó được thực hiện thông qua phương thức ảo. Vậy thì hãy cùng tìm hiểu phương thức ảo là gì?\n\n# Phương thức ảo\n\nPhương thức ảo (virtual method) trong C++ là cách thể hiện tính đa hình trong lập trình hướng đối tượng của C++, các phương thức ở class cơ sở có tính đa hình phải được định nghĩa là một phương thức ảo.\n\nVà để khai báo một phương thức ảo, ta khai báo như bình thường nhưng thêm từ khóa “virtual” phía trước.\n\n```cpp\nclass <ClassName>\n{\nprotected:\n    virtual <returnType> <methodName>([<params>]) {}\n};\n```\n\nVí dụ như trong bài toán ở trên, các phương thức tính lương, nhập và xuất đều có tính đa hình do đối với hai đối tượng NVSX và NVVP thì sẽ có cách thực hiện phương thức khác nhau. Do đó, mình sẽ chỉnh sửa 3 phương thức này ở class dẫn xuất thành phương thức ảo như sau:\n\n```cpp\nclass NhanVien\n{\npublic:\n    virtual void nhap()\n    {\n        // code goes here...\n    }\n \n    virtual void xuat()\n    {\n        // code goes here...\n    }\n \n    virtual void tinhLuong()\n    {\n        // code goes here...\n    }\n};\n```\n\nChúng ta đã có các phương thức ảo, vậy vấn đề hiện tại là làm sao để chương trình biết phương thức của đối tượng nào để thực hiện cho đúng? Thì trong C++, tính đa hình được thể hiện thông qua tham chiếu và con trỏ. Cụ thể như sau:\n\nĐối với tham chiếu, đối tượng tham chiếu của class cơ sở có thể tham chiếu đến đối tượng thuộc class dẫn xuất. Lúc này, khi gọi phương thức ảo, chương trình sẽ gọi đúng phương thức của đối tượng mà biến tham chiếu tham chiếu tới. Ví dụ:\n\n```cpp\nNhanVienVanPhong nvvp;\nNhanVien &nv = nv; // biến nv tham chiếu đến biến nvvp\n \nnv.nhap(); // Phương thức được gọi là sẽ phương thức nhập của class NhanVienVanPhong\n```\n\nDo để tham chiếu đến một biến, bắt buộc biến kia phải tồn tại trước, do đó cách tham chiếu rất ít khi được sử dụng mà thay vào đó, người ta sẽ dùng con trỏ.\n\nTrong bài [Kế thừa trong C++\n\n](https://khiemle.dev/ke-thua-trong-cpp/), mình đã có nói con trỏ của class cơ sở có thể trỏ đến đối tượng của class dẫn xuất, tính đa hình trong C++ chủ yếu đều thể hiện qua cách này. Cũng tương tự như việc tham chiếu, thực hiện qua con trỏ như sau:\n\n```cpp\nNhanVien *nv = new NhanVienVanPhong; // con trỏ nv trỏ tới biến động kiểu NhanVienVanPhong\n \nnv->nhap(); // Phương thức được gọi là sẽ phương thức nhập của class NhanVienVanPhong\n```\n\nQuay trở lại với bài toán ban đầu, giờ đây chúng ta đã có thể trả lời hai câu hỏi. Để lưu trữ 2 đối tượng mà chỉ dùng một vector chúng ta sẽ sử dụng con trỏ trỏ đến đối tượng thuộc class dẫn xuất. Các phương thức có thể được gọi theo đúng đối tượng thông qua con trỏ mà vector lưu trữ. Bây giờ chúng ta sẽ sửa lại như sau:\n\n```cpp\n// Thay\nvector<NhanVienVanPhong> NVVP;\nvector<NhanVienSanXuat> NVSX;\n// ở class CongTy Bằng\nvector<NhanVien *> NV;\n```\n\nDo thuộc tính thay đối, các phương thức có liên quan cũng thay đổi theo, ta sẽ sửa lại các phương thức của class CongTy không cần thực hiện trên hai vector khác nhau, hai đối tượng khác nhau nữa, chúng ta sẽ sử dụng tính đa hình, code như sau:\n\n```cpp\nvoid nhap()\n{\n    cout << \"Nhap so nhan vien: \";\n    int n;\n    cin >> n;\n    for (int i = 0; i < n; i++)\n    {\n        cout << \"Nhan vien van phong (1), nhan vien san xuat (2): \";\n        int k;\n        cin >> k;\n        NhanVien *nv;\n        // Tùy vào người dùng chọn đối tượng nào để nhập\n        if (k == 1)\n            nv = new NhanVienVanPhong;\n        else\n            nv = new NhanVienSanXuat;\n        nv->nhap(); // ta sẽ sử dụng hàm nhập của đối tượng mà người dùng chọn\n        this->NV.push_back(nv);\n    }\n}\n \nvoid xuat()\n{\n    cout << \"Nhan vien van phong:\" << endl;\n    for (int i = 0; i < this->NV.size(); i++)\n    {\n        cout << \"STT:\" << i + 1 << endl;\n        this->NV.at(i)->xuat(); // tùy vào đối tượng là gì mà phương thức xuất sẽ được gọi theo đúng đối tượng đó\n    }\n}\n \nvoid tinhLuong()\n{\n    for (int i = 0; i < this->NV.size(); i++)\n        this->NV.at(i)->tinhLuong(); // tùy vào đối tượng là gì mà phương thức tính lương sẽ được gọi theo đúng đối tượng đó\n}\n```\n\nCơ chế hoạt động của phương thức ảo là dựa trên liên kết động, có thể hiểu liên kết động là đối tượng không được xác định cụ thể khi biên dịch mà chỉ được xác định trong lúc chương trình thực thi.\n\nNói qua một chút về cách sử dụng tính đa hình với hàm. Giả sử mình muốn tính lương độc lập cho một nhân viên, không cụ thể nhân viên nào mà sẽ tùy thuộc vào nhân viên bạn muốn tính, mình sẽ viết hàm như sau:\n\n```cpp\n// Cách 1: tham chiếu\nvoid tinhLuong(NhanVien &nv)\n{\n    nv.tinhLuong();\n}\n \n// Cách 2: con trỏ (nên dùng)\nvoid tinhLuong(NhanVien *nv)\n{\n   nv->tinhLuong();\n}\n```\n\nRất tiện lợi phải không, thay vì việc phải viết hai hàm để gọi cho đúng phương thức theo từng đối tượng khác nhau.\n\n**Lưu ý**: tính đa hình chỉ sử dụng được thông qua tham số là tham chiếu hoặc con trỏ, nếu bạn sử dụng một tham số bình thường, phương thức của class cơ sở sẽ được gọi vì về bản chất nó là một đối tượng thuộc kiểu class cơ sở.\n\n```cpp\nvoid tinhLuong(NhanVien nv)\n{\n    nv.tinhLuong(); // tham số nv thuộc class NhanVien nên phương thức tính lương của class NhanVien sẽ được gọi\n}\n```\n\n# Phương thức thuần ảo\n\nNếu như phân tích kỹ thêm một chút, bạn sẽ thấy là class nhân viên thì phương thức tính lương không có bởi vì ta không thể tính lương cho một nhân viên mà không biết là nhân viên gì. Các class dẫn xuất của nó là một cụ thể hóa của class cơ sở nhân viên, nên ta có thể tính lương được, ta biết cách tính cho từng loại nhân viên như thế nào.\n\nTừ những điều trên, ta có khái niệm phương thức thuần ảo. Phương thức thuần ảo (pure virtual method) là phương thức ảo không có phần định nghĩa và bắt buộc phải được override ở class dẫn xuất. Để khai báo phương thức thuần ảo, ta cũng khai báo tương tự như phương thức ảo nhưng thay vì định nghĩa phần thân hàm, ta sẽ cho nó bằng 0.\n\n```cpp\nclass <ClassName>\n{\npublic:\n    virtual <returnType> <methodName>([<params>]) = 0;\n};\n```\n\nQuay lại với bài toán của chúng ta, class nhân viên sẽ không cần có hàm tính lương cụ thể do ta không biết tính như thế nào khi không xác định được loại nhân viên. Và ở class dẫn xuất, chúng ta cần có cách tính lương khác nhau cho mỗi loại nhân viên, từ đó suy ra phương thức tính lương sẽ là phương thức thuần ảo. Chúng ta sẽ sửa lại như sau:\n\n```cpp\nclass NhanVien\n{\npublic:\n    // code goes here...\n    virtual void tinhLuong() = 0;\n};\n```\n\nKhi mà phương thức tính lương trở thành thuần ảo, ở class dẫn xuất bắt buộc cần phải override lại phương thức này. Chúng ta vẫn thực hiện override bình thường. Do ở class dẫn xuất mình đã override rồi nên không cần sửa gì cả.\n\n# Lớp trừu tượng\n\nLớp trừu tượng (abstract class) là một class chứa phương thức thuần ảo, không thể tạo một đối tượng thuộc lớp trừu tượng. Điều này hiển nhiên, bởi vì nếu class chứa một hoặc nhiều phương thức ảo, khi đối tượng được tạo thì phương thức đó sẽ không có định nghĩa và sẽ gây lỗi.\n\nMột lớp trừu tượng phải có ít nhất một phương thức thuần ảo và nó có thể cùng có các phương thức và phương thức ảo khác. Các phương thức và phương thức ảo thuộc lớp trừu tượng vẫn có thể được gọi từ class dẫn xuất qua toán tử phạm vi (::) như bình thường.\n\nLớp trừu tượng làm cho logic thực tế trở nên hợp lý hơn trong lập trình. Như đã nói ở trên, bạn không thể tính lương khi không biết nhân viên đó là nhân viên gì, do đó, theo logic, ta không thể tạo đối tượng nhân viên được. Hay nói đúng hơn, ta không nên cho phép người lập trình có thể tạo ra đối tượng nhân viên bằng cách làm cho lớp đó trở thành lớp thuần ảo.\n\nVì phương thức nhập, xuất vẫn không cần override lại hoàn toàn, do đó mình sẽ giữ nguyên, chỉ đổi phương thức tính tiền thành phương thức thuần ảo thôi.\n\n```cpp\nclass NhanVien\n{\npublic:\n    // Code goes here...\n    virtual void tinhLuong() = 0;\n};\n```\n\nLúc này, bất kỳ class nào kế thừa từ class nhân viên đều phải override lại phương thức tính lương. Và bạn cũng không thể tạo được đối tượng nhân viên nữa. Ví dụ:\n\n```cpp\nclass NhanVienKiemToan : public NhanVien\n{\npublic:\n    void tinhLuong() // override method tinhLuong\n    {\n        // Nếu không override sẽ nhận thông báo lỗi\n    }\n};\n```\n\n```cpp\nNhanVien nx; // lỗi vì không thể tạo đối tượng thuộc abstract class\n```\n\nLưu ý là con trỏ class nhân viên vẫn có thể tạo được bình thường (nếu không thì sao sử dụng được đa hình :D).\n\n# Interface\n\nPhần này giới thiệu thêm cho các bạn thôi bởi vì trong C++ không hỗ trợ sẵn concept interface, các bạn nên tìm hiểu concept của các ngôn ngữ hiện đại khác như C#, Java… Tuy nhiên ta vẫn có thể sử dụng concept của các ngôn ngữ khác cho C++.\n\nInterface có thể được hiểu là một class trừu tượng hoàn toàn, nghĩa là mọi phương thức của interface đều là phương thức thuần ảo và bạn sẽ cần phải override lại tất cả các phương thức của interface ở class dẫn xuất. Ví dụ:\n\n```cpp\nclass Animal // Animal là một Interface\n{\nprotected:\n    string sound;\npublic:\n    virtual void move() = 0;\n    virtual void makeNoise() = 0;\n    virtual void eat() = 0;\n};\n \n// Class kế thừa từ Animal đều phải override toàn bộ\nclass Dog\n{\npublic:\n    Dog()\n    {\n        sound = \"Woo - Woo!\";\n    }\n    virtual void move()\n    {\n        // code goes here...\n    }\n \n    virtual void makeNoise()\n    {\n        // code goes here...\n    }\n \n    virtual void eat()\n    {\n        // code goes here...\n    }\n};\n```\n\n# Tổng kết\n\nVậy là trong bài viết này, mình đã giới thiệu cho các bạn về tính đa hình trong C++ và các vấn đề liên quan như phương thức ảo, thuần ảo, lớp trừu tượng và interface. Hy vọng là bài viết có ích với mọi người, đừng quên chia sẻ bài viết để mọi người cùng biết nha. Nếu có bất kỳ thắc mắc nào các bạn hãy để lại comment phía bên dưới mình sẽ phản hồi. Cảm ơn các bạn đã theo dõi bài viết!\n\n# Source Code\n\n```cpp\n#include <iostream>\nusing namespace std;\n \n#include <vector>\n#include <string>\n \nclass NhanVien\n{\nprotected:\n    string hoTen;\n    float luong;\n \npublic:\n    NhanVien()\n    {\n        this->hoTen = \"\";\n        this->luong = 0.0;\n    }\n \n    virtual void nhap()\n    {\n        cout << \"Ho ten: \";\n        cin.ignore();\n        getline(cin, this->hoTen);\n    }\n \n    virtual void xuat()\n    {\n        cout << \"Ho ten: \";\n        cout << this->hoTen << endl;\n    }\n \n    virtual void tinhLuong() = 0;\n};\n \nclass NhanVienSanXuat : public NhanVien\n{\nprivate:\n    int soSanPham;\n    float tienCong1SP;\n \npublic:\n    NhanVienSanXuat() : NhanVien()\n    {\n        this->soSanPham = 0;\n        this->tienCong1SP = 0;\n    }\n \n    void nhap()\n    {\n        NhanVien::nhap();\n        cout << \"So san pham: \";\n        cin >> this->soSanPham;\n        cout << \"Tien cong 1 san pham: \";\n        cin >> this->tienCong1SP;\n    }\n \n    void xuat()\n    {\n        cout << \"So san pham: \";\n        cout << this->soSanPham << endl;\n        cout << \"Tien cong 1 san pham: \";\n        cout << this->tienCong1SP << endl;\n        cout << \"Luong: \";\n        cout << this->luong << endl;\n    }\n \n    void tinhLuong()\n    {\n        this->luong = this->soSanPham * this->tienCong1SP;\n    }\n};\n \nclass NhanVienVanPhong : public NhanVien\n{\nprivate:\n    float luongCoBan;\n    int soNgayLamViec;\n \npublic:\n    NhanVienVanPhong() : NhanVien()\n    {\n        this->luongCoBan = 0.0;\n    }\n \n    void nhap()\n    {\n        NhanVien::nhap();\n        cout << \"Luong co ban: \";\n        cin >> this->luongCoBan;\n        cout << \"So ngay lam viec: \";\n        cin >> this->soNgayLamViec;\n    }\n \n    void xuat()\n    {\n        NhanVien::xuat();\n        cout << \"Luong co ban: \";\n        cout << this->luongCoBan << endl;\n        cout << \"So ngay lam viec: \";\n        cout << this->soNgayLamViec << endl;\n        cout << \"Luong: \";\n        cout << this->luong << endl;\n    }\n \n    void tinhLuong()\n    {\n        this->luong = this->soNgayLamViec * this->luongCoBan;\n    }\n};\n \nclass CongTy\n{\nprivate:\n    vector<NhanVien *> NV;\n \npublic:\n    void nhap()\n    {\n        cout << \"Nhap so nhan vien: \";\n        int n;\n        cin >> n;\n        for (int i = 0; i < n; i++)\n        {\n            cout << \"Nhan vien van phong (1), nhan vien san xuat (2): \";\n            int k;\n            cin >> k;\n            NhanVien *nv;\n            // Tùy vào người dùng chọn đối tượng nào để nhập\n            if (k == 1)\n                nv = new NhanVienVanPhong;\n            else\n                nv = new NhanVienSanXuat;\n            nv->nhap(); // ta sẽ sử dụng hàm nhập của đối tượng mà người dùng chọn\n            this->NV.push_back(nv);\n        }\n    }\n \n    void xuat()\n    {\n        cout << \"Nhan vien van phong:\" << endl;\n        for (int i = 0; i < this->NV.size(); i++)\n        {\n            cout << \"STT:\" << i + 1 << endl;\n            this->NV.at(i)->xuat(); // tùy vào đối tượng là gì mà phương thức xuất sẽ được gọi theo đúng đối tượng đó\n        }\n    }\n \n    void tinhLuong()\n    {\n        for (int i = 0; i < this->NV.size(); i++)\n            this->NV.at(i)->tinhLuong(); // tùy vào đối tượng là gì mà phương thức tính lương sẽ được gọi theo đúng đối tượng đó\n    }\n};\n \nint main()\n{\n    CongTy cty;\n    cty.nhap();\n    cty.tinhLuong();\n    cty.xuat();\n    return 0;\n}\n```\n\n# Tham khảo\n\n1. [Con trỏ và cấp phát động trong C++](/blog/con-tro-va-cap-phat-dong-trong-cpp)\n1. [Cơ bản về Class trong C++](/blog/co-ban-ve-class-trong-cpp)\n1. [Kế thừa trong C++](/blog/ke-thua-trong-cpp)\n1. [Vector trong C++](/blog/vector-trong-cpp)","tags":["Lập trình","C++"]},{"slug":"cac-thuat-toan-sap-xep-pho-bien","title":"Các thuật toán sắp xếp phổ biến","date":"2020-05-25","topic":"tutorial","summary":"Thuật toán sắp xếp là lời giải của bài toán sắp xếp, vậy thì trước tiên, ta hãy tìm hiểu xem bài toán sắp xếp là gì trước đã.","body":"![Các thuật toán sắp xếp phổ biến](/api/uploads/c8473b686475800e805b.jpg)\n\n# Bài toán sắp xếp\n\nThuật toán sắp xếp là lời giải của bài toán sắp xếp, vậy thì trước tiên, ta hãy tìm hiểu xem bài toán sắp xếp là gì trước đã.\n\nBài toán sắp xếp chắc chắn không còn xa lạ gì với mỗi chúng ta, nó là một trong những bài toán được bắt gặp nhiều nhất trong thực tế. Ví dụ như sắp xếp danh sách lớp học, sắp xếp quyển sách, sắp xếp tiền… Vậy thì bài toán sắp xếp là gì?\n\nBài toán sắp xếp là chúng ta sẽ sắp xếp lại các phần tử của một danh sách theo chiều tăng hoặc giảm dần theo một tiêu chí nào đó của phần tử trong danh sách.\n\nVí dụ như bạn sắp xếp danh sách lớp học theo điểm trung bình từ cao đến thấp, sắp những quyển sách theo kích cỡ từ nhỏ đến lớn, sắp xếp những tờ tiền theo mệnh giá từ thấp đến cao…\n\nMục đích của việc sắp xếp chính là giúp ta có cái nhìn tổng quan hơn về những dữ liệu mà ta có, dễ dàng tìm kiếm những phần tử đứng nhất về một tiêu chí nào đó như mình đã nói trong *Các thuật toán tìm kiếm phổ biến*[\\[1\\]](#tham-khao), hầu như mọi bài toán đều quy về bài toán tìm kiếm. Ví dụ:\n\nBạn có một danh sách lớp học chưa được sắp xếp, bạn muốn biết được là mức độ đề thi có khó đối với học sinh hay không, top 3 học sinh có điểm trung bình cao nhất. Vậy thì sau khi bạn thực hiện việc sắp xếp giảm theo điểm trung bình, bạn sẽ dễ dàng đánh giá được mức độ của đề đối với học sinh là dễ hay khó thông qua việc nhìn vào đầu và cuối danh sách, đầu danh sách điểm không cao lắm và cuối danh sách điểm thấp thì chắc chắn đề này khó đối với học sinh và ngược lại.\n\nTrong lập trình, sắp xếp không chỉ đơn giản là để tìm một hoặc nhiều phần tử đứng đầu về một tiêu chí nào đó hay để có cái nhìn tổng quan về dữ liệu, sắp xếp còn làm cơ sở cho các giải thuật nâng cao với hiệu suất cao hơn.\n\nVí dụ như khi thực hiện tìm kiếm, thuật toán tìm kiếm nhị phân có độ phức tạp thời gian là O(log(n)) và ổn định, nhưng thuật toán này chỉ áp dụng được với dãy đã được sắp xếp. Vậy khi này, bạn có thể thực hiện sắp xếp trước sau đó áp dụng thuật toán tìm kiếm nhị phân.\n\nBài toán sắp xếp chỉ đơn giản có vậy, bây giờ mình sẽ giới thiệu đến các bạn một số giải thuật tìm kiếm phổ biến nhất mà lập trình viên nào cũng nên biết. Hãy cùng bắt đầu thôi!\n\nLưu ý trước khi đọc bài: bạn cần có kỹ năng lập trình C++ cơ bản, hiểu về độ phức tạp của thuật toán. Trong bài viết có sử dụng từ thuật toán sắp xếp ổn định, thuật toán sắp xếp ổn định nghĩa là thứ tự của các phần tử có cùng giá trị sẽ không thay đổi so với ban đầu. Ví dụ như 1 5 3 3 4, sau khi sắp xếp cũng là 1 3 3 4 5.\n\n# Sắp xếp nổi bọt (Bubble Sort)\n\nSắp xếp nổi bọt hay bubble sort là thuật toán sắp xếp đầu tiên mà mình giới thiệu đến các bạn và cũng là thuật toán đơn giản nhất trong các thuật toán mà mình sẽ giới thiệu, ý tưởng của thuật toán này như sau:\n\nDuyệt qua danh sách, làm cho các phần tử lớn nhất hoặc nhỏ nhất dịch chuyển về phía cuối danh sách, tiếp tục lại làm phần tử lớn nhất hoặc nhỏ nhất kế đó dịch chuyển về cuối hay chính là làm cho phần tử nhỏ nhất (hoặc lớn nhất) nổi lên, cứ như vậy cho đến hết danh sách Cụ thể các bước thực hiện của giải thuật này như sau:\n\n1.  Gán i = 0\n2.  Gán j = 0\n3.  Nếu A\\[j\\] > A\\[j + 1\\] thì đối chỗ A\\[j\\] và A\\[j + 1\\]\n4.  Nếu j < n - i - 1:\n    -   Đúng thì j = j + 1 và quay lại bước 3\n    -   Sai thì sang bước 5\n5.  Nếu i < n - 1:\n    -   Đúng thì i = i + 1 và quay lại bước 2\n    -   Sai thì dừng lại\n\nThật đơn giản đúng không nào, chúng ta hãy cùng cài đặt thuật toán này trong C++ nha.\n\n```cpp\nvoid BubbleSort(int A[], int n)\n{\n    for (int i = 0; i < n - 1; i++)\n        for (int j = 0; j < n - i - 1; j++)\n            if (A[j] > A[j + 1])\n                swap(A[j], A[j + 1]); // đổi chỗ A[j] và A[j + 1]\n}\n```\n\nSắp xếp nổi bọt là một thuật toán sắp xếp ổn định. Về độ phức tạp, do dùng hai vòng lặp lồng vào nhau nên độ phức tạp thời gian trung bình của thuật toán này là O(n2).\n\nCác bạn có thể xem mình trình bày ý tưởng của giải thuật này trong bên dưới:\n\n[Youtube: Thuật toán sắp xếp nổi bọt | Khiêm Lê\n\n](https://youtu.be/S2le_2BTEAc)\n\nĐừng quên đăng ký kênh Youtube [Khiêm Lê\n\n](https://khiemle.dev/youtube) để ủng hộ mình nha!\n\n# Sắp xếp chọn (Selection Sort)\n\nSắp xếp chọn hay selection sort sẽ là thuật toán thứ hai mà mình giới thiệu đến các bạn, ý tưởng của thuật toán này như sau: duyệt từ đầu đến phần tử kề cuối danh sách, duyệt tìm phần tử nhỏ nhất từ vị trí kế phần tử đang duyệt đến hết, sau đó đổi vị trí của phần tử nhỏ nhất đó với phần tử đang duyệt và cứ tiếp tục như vậy.\n\nCho mảng A có n phần tử chưa được sắp xếp. Cụ thể các bước của giải thuật này áp dụng trên mảng A như sau:\n\n1.  Gán i = 0\n2.  Gán j = i + 1 và min = A\\[i\\]\n3.  Nếu j < n:\n    -   Nếu A\\[j\\] < A\\[min\\] thì min = j\n    -   j = j + 1\n    -   Quay lại bước 3\n4.  Đổi chỗ A\\[min\\] và A\\[i\\]\n5.  Nếu i < n - 1:\n    -   Đúng thì i = i + 1 và quay lại bước 2\n    -   Sai thì dừng lại\n\nÝ tưởng và từng bước giải cụ thể đã có, bây giờ mình sẽ sử dụng thuật toán này trong C++:\n\n```cpp\nvoid SelectionSort(int A[], int n)\n{\n    int min;\n    for (int i = 0; i < n - 1; i++)\n    {\n        min = i; // tạm thời xem A[i] là nhỏ nhất\n        // Tìm phẩn tử nhỏ nhất trong đoạn từ A[i] đến A[n - 1]\n        for (int j = i + 1; j < n; j++)\n            if (A[j] < A[min]) // A[j] mà nhỏ hơn A[min] thì A[j] là nhỏ nhất\n                min = j; // lưu lại vị trí A[min] mới vừa tìm được\n        if (min != i) // nếu như A[min] không phải là A[i] ban đầu thì đổi chỗ\n            swap(A[i], A[min]);\n    }\n}\n```\n\nĐối với thuật toán sắp xếp chọn, do sử dụng 2 vòng lặp lồng vào nhau, độ phức tạp thời gian trung bình của thuật toán này là O(n2). Thuật toán sắp xếp chọn mình cài đặt là thuật toán sắp xếp không ổn định, nó còn có một phiên bản khác cải tiến là thuật toán sắp xếp chọn ổn định.\n\nGiải thích ý tưởng thuật toán:\n\n[Youtube: Thuật toán sắp xếp chọn | Khiêm Lê\n\n](https://youtu.be/oPkY5vKdol0)\n\n# Sắp xếp chèn (Insertion Sort)\n\nSắp xếp chèn hay insertion sort là thuật toán tiếp theo mà mình giới thiệu, ý tưởng của thuật toán này như sau: ta có mảng ban đầu gồm phần tử A\\[0\\] xem như đã sắp xếp, ta sẽ duyệt từ phần tử 1 đến n - 1, tìm cách chèn những phần tử đó vào vị trí thích hợp trong mảng ban đầu đã được sắp xếp.\n\nGiả sử cho mảng A có n phần tử chưa được sắp xếp. Các bước thực hiện của thuật toán áp dụng trên mảng A như sau:\n\n1.  Gán i = 1\n2.  Gán x = A\\[i\\] và pos = i - 1\n3.  Nếu pos >= 0 và A\\[pos\\] > x:\n    -   A\\[pos + 1\\] = A\\[pos\\]\n    -   pos = pos - 1\n    -   Quay lại bước 3\n4.  A\\[pos + 1\\] = x\n5.  Nếu i < n:\n    -   Đúng thì i = i + 1 và quay lại bước 2\n    -   Sai thì dừng lại\n\nBây giờ thì áp dụng nó vào trong C++ thôi!\n\n```cpp\nvoid InsertionSort(int A[], int n)\n{\n    int pos, x;\n    for (int i = 1; i < n; i++)\n    {\n        x = A[i]; // lưu lại giá trị của x tránh bị ghi đè khi dịch chuyển các phần tử\n        pos = i - 1;\n        // tìm vị trí thích hợp để chèn x\n        while (pos >= 0 && A[pos] > x)\n        {\n            // kết hợp với dịch chuyển phần tử sang phải để chừa chỗ cho x\n            A[pos + 1] = A[pos];\n            pos--;\n        }\n        // chèn x vào vị trí đã tìm được\n        A[pos + 1] = x;\n    }\n}\n```\n\nCũng tương tự như sắp xếp chọn, thuật toán sắp xếp chèn cũng có độ phức tạp thời gian trung bình là O(n2) do có hai vòng lặp lồng vào nhau.\n\nVideo giải thích ý tưởng thuật toán:\n\n[Youtube: Thuật toán sắp xếp chèn | Khiêm Lê\n\n](https://youtu.be/5tpQ6x1JEXE)\n\n# Sắp xếp trộn (Merge Sort)\n\nSắp xếp trộn (merge sort) là một thuật toán dựa trên kỹ thuật chia để trị, ý tưởng của thuật toán này như sau: chia đôi mảng thành hai mảng con, sắp xếp hai mảng con đó và trộn lại theo đúng thứ tự, mảng con được sắp xếp bằng cách tương tự.\n\nGiả sử left là vị trí đầu và right là cuối mảng đang xét, cụ thể các bước của thuật toán như sau:\n\n-   Nếu mảng còn có thể chia đôi được (tức left < right)\n    1.  Tìm vị trí chính giữa mảng\n    2.  Sắp xếp mảng thứ nhất (từ vị trí left đến mid)\n    3.  Sắp xếp mảng thứ 2 (từ vị trí mid + 1 đến right)\n    4.  Trộn hai mảng đã sắp xếp với nhau\n\nBây giờ mình sẽ cài đặt thuật toán cụ thể trong C++ như sau:\n\n```cpp\n// Hàm trộn hai mảng con vào nhau theo đúng thứ tự\nvoid Merge(int A[], int left, int mid, int right)\n{\n    int n1 = mid - left + 1; // Số phần tử của mảng thứ nhất\n    int n2 = right - mid; // Số phần tử của mảng thứ hai\n \n    // Tạo hai mảng tạm để lưu hai mảng con\n    int *LeftArr = new int[n1];\n    int *RightArr = new int[n2];\n \n    // Sao chép phần tử 2 mảng con vào mảng tạm\n    for (int i = 0; i < n1; i++)\n        LeftArr[i] = A[left + i];\n    for (int i = 0; i < n2; i++)\n        RightArr[i] = A[mid + 1 + i];\n \n    // current là vị trí hiện tại trong mảng A\n    int i = 0, j = 0, current = left;\n \n    // Trộn hai mảng vào nhau theo đúng thứ tự\n    while (i < n1 && j < n2)\n        if (LeftArr[i] <= RightArr[j])\n            A[current++] = LeftArr[i++];\n        else\n            A[current++] = RightArr[j++];\n \n    // Nếu mảng thứ nhất còn phần tử thì copy nó vào mảng A\n    while (i < n1)\n        A[current++] = LeftArr[i++];\n \n    // Nếu mảng thứ hai còn phần tử thì copy nó vào mảng A\n    while (j < n2)\n        A[current++] = RightArr[j++];\n \n    // Xóa hai mảng tạm đi\n    delete[] LeftArr, RightArr;\n}\n \n// Hàm chia đôi mảng và gọi hàm trộn\nvoid _MergeSort(int A[], int left, int right)\n{\n    // Kiểm tra xem còn chia đôi mảng được không\n    if (left < right)\n    {\n        // Tìm phần tử chính giữa\n        // left + (right - left) / 2 tương đương với (left + right) / 2\n        // việc này giúp tránh bị tràn số với left, right quá lớn\n        int mid = left + (right - left) / 2;\n \n        // Sắp xếp mảng thứ nhất\n        _MergeSort(A, left, mid);\n        // Sắp xếp mảng thứ hai\n        _MergeSort(A, mid + 1, right);\n \n        // Trộn hai mảng đã sắp xếp\n        Merge(A, left, mid, right);\n    }\n}\n \n// Hàm sắp xếp chính, được gọi khi dùng merge sort\nvoid MergeSort(int A[], int n)\n{\n    _MergeSort(A, 0, n - 1);\n}\n```\n\nVề độ phức tạp, thuật toán Merge Sort có độ phức tạp thời gian trung bình là O(nlog(n)), về không gian, do sử dụng mảng phụ để lưu trữ, và 2 mảng phụ dài nhất là hai mảng phụ ở lần chia đầu tiên có tổng số phần tử bằng đúng số phần tử của mảng nên độ phức tạp sẽ là O(n). Sắp xếp trộn là thuật toán sắp xếp ổn định.\n\nVideo minh họa của GeeksforGeeks:\n\n[Youtube: Thuật toán sắp xếp trộn | Khiêm Lê\n\n](https://youtu.be/pJE0a_XVFok)\n\n# Sắp xếp nhanh (Quick Sort)\n\nSắp xếp nhanh (quick sort) hay sắp xếp phân đoạn (Partition) là là thuật toán sắp xếp dựa trên kỹ thuật chia để trị, cụ thể ý tưởng là: chọn một điểm làm chốt (gọi là pivot), sắp xếp mọi phần tử bên trái chốt đều nhỏ hơn chốt và mọi phần tử bên phải đều lớn hơn chốt, sau khi xong ta được 2 dãy con bên trái và bên phải, áp dụng tương tự cách sắp xếp này cho 2 dãy con vừa tìm được cho đến khi dãy con chỉ còn 1 phần tử.\n\nCụ thể áp dụng thuật toán cho mảng như sau:\n\n1.  Chọn một phần tử làm chốt\n2.  Sắp xếp phần tử bên trái nhỏ hơn chốt\n3.  Sắp xếp phần tử bên phải nhỏ hơn chốt\n4.  Sắp xếp hai mảng con bên trái và bên phải pivot\n\nPhần tử được chọn làm chốt rất quan trọng, nó quyết định thời gian thực thi của thuật toán. Phần tử được chọn làm chốt tối ưu nhất là phần tử trung vị, phần tử này làm cho số phần tử nhỏ hơn trong dãy bằng hoặc sấp xỉ số phần tử lớn hơn trong dãy. Tuy nhiên, việc tìm phần tử này rất tốn kém, phải có thuật toán tìm riêng, từ đó làm giảm hiệu suất của thuật toán tìm kiếm nhanh, do đó, để đơn giản, người ta thường sử dụng phần tử chính giữa làm chốt.\n\nTrong bài viết này, mình cũng sẽ sử dụng phần tử chính giữa làm chốt, thuật toán cài đặt trong C++ như sau:\n\n```cpp\nvoid Partition(int A[], int left, int right)\n{\n    // Kiểm tra xem nếu mảng có 1 phần tử thì không cần sắp xếp\n    if (left >= right)\n        return;\n \n    int pivot = A[(left + right) / 2]; // Chọn phần tử chính giữa dãy làm chốt\n \n    // i là vị trí đầu và j là cuối đoạn\n    int i = left, j = right;\n    while (i < j)\n    {\n        while (A[i] < pivot) // Nếu phần tử bên trái nhỏ hơn pivot thì ok, bỏ qua\n            i++;\n        while (A[j] > pivot) // Nếu phần tử bên phải nhỏ hơn pivot thì ok, bỏ qua\n            j--;\n \n        // Sau khi kết thúc hai vòng while ở trên thì chắc chắn\n        // vị trí A[i] phải lớn hơn pivot và A[j] phải nhỏ hơn pivot\n        // nếu i < j\n        if (i <= j)\n        {\n            if (i < j) // nếu i != j (tức không trùng thì mới cần hoán đổi)\n                swap(A[i], A[j]); // Thực hiện đổi chổ ta được A[i] < pivot và A[j] > pivot\n            i++;\n            j--;\n        }\n    }\n \n    // Gọi đệ quy sắp xếp dãy bên trái pivot\n    Partition(A, left, j);\n    // Gọi đệ quy sắp xếp dãy bên phải pivot\n    Partition(A, i, right);\n}\n \n// Hàm sắp xếp chính\nvoid QuickSort(int A[], int n)\n{\n    Partition(A, 0, n - 1);\n}\n```\n\nThuật toán sắp xếp nhanh không phải là thuật toán sắp xếp ổn định, tuy nhiên vẫn có thể cải tiến nó thành thuật toán sắp xếp ổn định. Độ phức tạp thời gian trung bình của thuật toán này là O(nlog(n)).\n\n[Youtube: Thuật toán sắp xếp nhanh | Khiêm Lê\n\n](https://youtu.be/kPQfMUtMVjU)\n\n## Một số thuật toán sắp xếp khác\n\nNgoài các thuật toán trên (được in đậm bên dưới), bạn có thể tìm hiểu thêm một số thuật toán sắp xếp khác bên dưới:\n\n1.  **[Selection Sort - Sắp xếp chọn](#sap-xep-chon-selection-sort)**\n2.  **[Insertion Sort - Sắp xếp chèn](#sap-xep-chen-insertion-sort)**\n3.  Binary Insersion Sort - Chèn nhị phân\n4.  Interchange Sort - Đổi chỗ trực tiếp\n5.  **[Bubble Sort - Sắp xếp nổi bọt](#sap-xep-noi-bot-bubble-sort)**\n6.  Shaker Sort\n7.  Shell Sort\n8.  Heap Sort - Sắp xếp vun đống\n9.  **[Quick Sort - Sắp xếp nhanh](#sap-xep-nhanh-quick-sort)**\n10.  **[Merge Sort - Sắp xếp trộn](#sap-xep-tron-merge-sort)**\n11.  Counting Sort - Sắp xếp đếm\n12.  Radix Sort - Sắp xếp cơ số\n\nCác bạn có thể tham khảo thêm các thuật toán trên ở trang *GeeksforGeeks*[\\[2\\]](#tham-khao) nhé!\n\n# Lời kết\n\nVậy là qua bài viết này, mình đã giới thiệu đến các bạn các thuật toán tìm kiếm phổ biến nhất. Nếu bạn có thắc mắc hoặc góp ý nào, đừng quên để lại bình luận bên dưới bài viết. Đừng quên chia sẻ bài viết này cho bạn bè cùng biết nha. Cảm ơn các bạn đã theo dõi bài viết!\n\n# Tham khảo\n\n1. [Các thuật toán tìm kiếm phổ biến](/blog/cac-thuat-toan-tim-kiem-pho-bien)\n1. [GeeksforGeeks ](https://www.geeksforgeeks.org/)","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"ke-thua-trong-cpp","title":"Kế thừa trong C++","date":"2020-05-12","topic":"tutorial","summary":"Kế thừa trong lập trình hướng đối tượng là một tính chất rất quan trọng, do đó, các bạn nên nắm kỹ phần này để có thể học tốt lập trình hướng đối tượng. Trong các kỳ kiểm tra, phỏng vấn xin việc cũng thường được hỏi về tính chất này.","body":"![Kế thừa trong C++](/api/uploads/282f31bdb7f54a401092.jpg)\n\nKế thừa trong lập trình hướng đối tượng là một tính chất rất quan trọng, do đó, các bạn nên nắm kỹ phần này để có thể học tốt lập trình hướng đối tượng. Trong các kỳ kiểm tra, phỏng vấn xin việc cũng thường được hỏi về tính chất này.\n\nĐể đọc hiểu bài này tốt nhất, các bạn nên có biến thức *Cơ bản về class trong C++*[\\[1\\]](#tham-khao), nếu chưa hãy dành một ít thời gian đọc bài viết về class của mình. Nếu bạn đã sẵn sàng thì hãy bắt đầu thôi!\n\n# Kế thừa là gì?\n\nKế thừa là một trong các tính chất đặc trưng của lập trình hướng đối tượng, bên cạnh tính đóng gói (encapsulation), che giấu thông tin (hiding information), tính đa hình (polymorphism) và tính trừu tượng (abstraction). Vậy thì kế thừa là gì?\n\nKế thừa (inheritance) là một tính chất đặc trưng của lập trình hướng đối tượng. Nó có nghĩa là một class thừa hưởng lại tất cả các thuộc tính, phương thức của class mà nó kế thừa.\n\nClass kế thừa từ một class khác gọi là lớp con (child class hay subclass) hay lớp dẫn xuất (derived class). Class được lớp khác kế thừa được gọi là lớp cha (parent class hay superclass) hay lớp cơ sở (base class).\n\nVí dụ như bạn có một class con người, có các thuộc tính cơ bản như họ tên, ngày sinh, quê quán, mình khai báo thêm một class sinh viên kế thừa từ class con người. Vậy, class sinh viên sẽ có các thuộc tính họ tên, ngày sinh, quê quán từ class con người mà không cần phải khai báo. Class con người sẽ là lớp cha và class sinh viên là lớp con.\n\nNgoài các thuộc tính của class cha, class con còn có thể có thêm các thuộc tính, phương thức của riêng mình. Ví dụ như sinh viên thì có thêm các thuộc tính như MSSV, tên trường, chuyên ngành…\n\n![Tính kế thừa](/api/uploads/23c23bbf3bdc9f2e1ae6.jpg)\n\nTính kế thừa\n\nChúng ta đã hiểu được kế thừa là gì rồi. Vậy thì câu hỏi đặt ra lúc này là tại sao cần có kế thừa? Hãy cùng tìm hiểu tiếp nhé.\n\n# Tại sao cần có kế thừa?\n\nTrong lập trình, chắc hẳn các bạn sẽ gặp phải rất nhiều hành động lặp đi lặp lại, và chắc chắn một điều rằng sẽ không ai rảnh mà ngồi code lại chúng từ đầu cả. Thay vào đó, họ sẽ sử dụng lại các đoạn code đã có để thực hiện công việc tương tự. Nói đến đây chắc các bạn cũng đã hiểu kế thừa có tác dụng gì rồi đúng không nào!\n\nKế thừa giúp ta có thể tái sử dụng lại những đoạn code đã có, tránh việc giải quyết lại các bài toán con đã có lời giải trước đó, gây lãng phí thời gian. Nếu chỉ là một, hai hoặc ba hành động cần giải quyết thì không thành vấn đề, nhưng nếu là một trăm thì đó lại là chuyện khác, sẽ tốn của bạn kha khá thời gian nếu bạn không biết tận dụng những gì đã có đấy!\n\nTrong ví dụ ở đầu bài viết, các bạn có để ý thấy mối quan hệ giữa class con người và sinh viên không? Đúng vậy, mối quan hệ đó là “sinh viên là một con người”, sinh viên cũng có các thông tin như họ tên, ngày sinh, quê quán… và được biểu diễn bằng các thuộc tính trong class. Vậy thì tính kế thừa giúp cho chúng ta có thể dễ dàng thể hiện mối quan hệ giữa các đối tượng hơn (sẽ được trình bày bên dưới).\n\nSinh viên ngoài có những đặc điểm của class con người ra, còn có thể có thêm các đặc điểm của riêng mình ví dụ như tên trường, tên ngành, năm học… Đây là mối quan hệ tổng quát hóa - đặc biệt hóa trong kế thừa. Class con người chính là tổng quát hóa và class con sinh viên chính là đặc biệt hóa.\n\n# Mối quan hệ giữa các đối tượng\n\nCó hai mối quan hệ giữa các đối tượng trong C++, quan hệ “Has-A” và quan hệ “Is-A”.\n\n## Quan hệ Has-A\n\nCó 3 loại quan hệ “has a” giữa các đối tượng, mối quan hệ một - một, mối quan hệ một - nhiều và mối quan hệ nhiều - nhiều.\n\nQuan hệ một - một (1 - 1) là mối quan hệ giữa hai đối tượng thuộc hai class khác nhau, mỗi đối tượng thuộc class này có quan hệ duy nhất với một đối tượng thuộc class kia và tương tự mỗi đối tượng của class kia cũng chỉ có quan hệ duy nhất với một đối tượng thuộc class này.\n\nVí dụ như mỗi lớp chỉ có một giáo viên chủ nhiệm, mỗi giáo viên chỉ được chủ nhiệm một lớp, mỗi một quốc gia chỉ có một thủ đô, một thành phố chỉ có thể là thủ đô của một quốc gia…\n\nQuan hệ một - nhiều (1 - n) là mối quan hệ giữa đối tượng thuộc hai class khác nhay, mỗi đối tượng thuộc class này có quan hệ duy nhất với một đối tượng thuộc class kia, nhưng mỗi đối tượng thuộc class kia có thể có quan hệ với nhiều đối tượng thuộc class này.\n\nVí dụ như mỗi học sinh chỉ có một lớp học, nhưng mỗi lớp học lại có thể có nhiều học sinh, hay mỗi nhân viên chỉ có thể làm cho một công ty nhưng một công ty lại có thể có nhiều nhân viên…\n\nQuan hệ nhiều - nhiều (n - n) là mối quan hệ giữa đối tượng thuộc hai class khác nhau, mỗi đối tượng thuộc class này có thể quan hệ với nhiều đối tượng thuộc class kia, và mỗi đối tượng thuộc class kia cũng có thể có quan hệ với nhiều đối tượng thuộc lớp này.\n\nVí dụ như mỗi bệnh nhân có thể thăm khám bệnh ở nhiều bác sĩ khác nhau, và mỗi bác sĩ lại có thể chữa cho nhiều bệnh nhân…\n\nQuan hệ tổng quát - đặc biệt hóa là mối quan hệ giữa đối tượng thuộc hai class khác nhau khi đối tượng thuộc class này là một trường hợp đặc biệt của class kia, và đối tượng thuộc class kia là trường hợp tổng quát của đối tượng thuộc class này.\n\nVí dụ như con người là tổng quát hóa của sinh viên, còn sinh viên là đặc biệt hóa của con người, xe máy hiệu exciter là đặc biệt hóa của xe máy và xe máy là tổng quát của xe máy hiệu exciter…\n\n## Quan hệ Is-A\n\nMối quan hệ Is-A giữa các đối tượng chính là class này là một class kia. Cũng ví dụ từ đầu bài viết đến giờ, sinh viên là một con người (student **is a** human). Đó chính là mối quan hệ có được do thực hiện kế thừa.\n\n# Các loại kế thừa\n\nChúng ta đã cùng tìm hiểu qua về kế thừa là gì và tại sao cần có kế thừa, tiếp theo, hãy cùng tìm hiểu xem có những loại kế thừa nào nha.\n\n## Kế thừa đơn\n\nKế thừa đơn (single inheritance) là một class con kế thừa duy nhất từ một class cha. Ví dụ như class sinh viên chỉ kế thừa duy nhất từ class con người, đó là một kế thừa đơn.\n\n![Đơn kế thừa](/api/uploads/23c23bbf3bdc9f2e1ae6.jpg)\n\nĐơn kế thừa\n\n## Kế thừa đa cấp\n\nKế thừa đa cấp (multilevel inheritance) là một class con kế thừa từ một class cha, class cha đó lại kết thừa từ một lớp khác. Ví dụ như class sinh viên kế thừa từ class học sinh, class học sinh lại kế thừa từ class con người, đó là một kế thừa đa cấp.\n\n![Kế thừa đa cấp](/api/uploads/bf0a45c77a0ccc29ecd9.jpg)\n\nKế thừa đa cấp\n\n## Kế thừa phân cấp\n\nKế thừa phân cấp (hierarchical inheritance) là khi có nhiều hơn một class con kế thừa từ class cha. Ví dụ như class sinh viên và class công nhân đều kế thừa từ class cha là con người.\n\n![Kế thừa phân cấp](/api/uploads/6ef3c034a50b89bc394e.jpg)\n\nKế thừa phân cấp\n\nMột sơ đồ class không chỉ có một trong 3 loại kế thừa trên, nó thậm chí có thể có cả 3 và điều này hoàn toàn bình thường và gặp rất thường xuyên.\n\n![Sơ đồ class](/api/uploads/68efb0974ac16c9b7d49.jpg)\n\nSơ đồ Class\n\nVậy là chúng ta đã tìm hiểu xong các loại kế thừa, giờ hãy xem cú pháp thực hiện kế thừa trong C++ như thế nào.\n\n# Cú pháp\n\nCú pháp để khai báo một lớp kế thừa từ một lớp như sau:\n\n```cpp\nclass <tên_class_con> : <phạm_vi_truy_cập> <tên_class_cha>\n{\n    // code goes here\n};\n```\n\nTrong đó, class cha và class con đã được trình bày ở bên trên, phạm vi truy cập sẽ được mình trình bày ngay bến dưới.\n\nGiả sử mình có một class A với các thuộc tính, phương thức bên trong. Mình muốn khai báo một class B kế thừa từ class A, mình sẽ có cú pháp như sau:\n\n```cpp\nclass B : public/private/protected A\n{\n    // code goes here\n};\n```\n\nKhi này, các thuộc tính trong class A sẽ được class B kế thừa. public/private/protected là phạm vi truy cập của các thuộc tính sẽ được kế thừa. Hãy cùng tìm hiểu xem cụ thể phạm vi truy cập là như thế nào ngay bên dưới.\n\n# Phạm vi truy cập\n\nTrong bài *Cơ bản về class trong C++*[\\[1\\]](#tham-khao) mình đã có giới thiệu qua về phạm vi truy cập (access modifier). Tuy nhiên, phạm vi truy cập trong bài đó khác với phạm vi truy cập trong kế thừa.\n\nCó hai loại phạm vi truy cập, truy cập các thành phần từ bên ngoài đối tượng được gọi là truy cập theo chiều ngang. Truy cập các thành phần của class cha từ class con được gọi là truy cập theo chiều dọc. Phạm vi truy cập trong bài *Cơ bản về class trong C++*[\\[1\\]](#tham-khao) chính là truy cập theo chiều ngang.\n\nĐối với phạm vi truy cập trong kế thừa, đó là truy cập theo chiều dọc. Giả sử mình có class B kế thừa từ class A, cụ thể các phạm vi truy cập đó là như sau:\n\n-   public:\n    -   Các thuộc tính public của A sẽ trở thành thuộc tính public của B\n    -   Các thuộc tính protected của A sẽ trở thành protected của B\n-   private:\n    -   Các thuộc tính public của A sẽ trở thành thuộc tính private của B\n    -   Các thuộc tính protected của A sẽ trở thành private của B\n-   protected:\n    -   Các thuộc tính public của A sẽ trở thành thuộc tính protected của B\n    -   Các thuộc tính protected của A sẽ trở thành thuộc tính protected của B\n\n![Phạm vi truy cập trong kế thừa C++](/api/uploads/59fbdef207d15f9336eb.jpg)\n\nPhạm vi truy cập trong kế thừa C++\n\nĐối với một class, thuộc tính protected cũng tương tự như private, chỉ có khác ở chỗ class con có thể truy cập thành phần protected, còn private lại không cho phép truy cập.\n\nPhạm vi truy cập giúp đảm bảo tính đóng gói và che giấu thông tin của đối tượng. Ví dụ như khi thuộc tính public ở class cha, được kế thừa private sang class con, thì nó cũng trở thành private ở class con và không thể được truy cập từ bên ngoài class con. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    int publicMethod;\n};\n \nclass B : private A\n{\n};\n \n// bên trong hàm main\nB b;\ncout << b.publicMethod; // lỗi vì lúc này publicMethod trở thành private method của B rồi\n```\n\n# Cài đặt\n\nTừ đầu bài viết đến giờ, mình nói khá nhiều về lý thuyết mà chưa có đoạn code nào cả. Để cho các bạn dễ hiểu hơn, mình sẽ sử dụng lại các ví dụ trong bài viết và code chúng bằng C++. Hãy bắt đầu với các quan hệ Has-A trước.\n\nQuan hệ 1 - 1:\n\n```cpp\nclass GVCN\n{\npublic:\n    LopHoc lopHoc;\n};\n \nclass LopHoc\n{\npublic:\n    GVCN gvcn;\n};\n```\n\nBạn nào chưa biết về vector có thể xem lại bài viết *Vector trong C++*[\\[2\\]](#tham-khao) của mình nha.\n\nQuan hệ 1 - n:\n\n```cpp\nclass HocSinh\n{\npublic:\n    LopHoc lopHoc;\n};\n \nclass LopHoc\n{\npublic:\n    vector<HocSinh> hocSinh;\n};\n```\n\nQuan hệ n - n:\n\n```cpp\nclass BenhNhan\n{\npublic:\n    vector<BacSi> bacSi;\n};\n \nclass BacSi\n{\npublic:\n    vector<BenhNhan> benhNhan;\n};\n```\n\nQuan hệ Is-A:\n\n```cpp\nclass ConNguoi\n{\npublic:\n    string hoTen;\n    string ngaySinh;\n    string queQuan;\n};\n \nclass SinhVien : public ConNguoi\n{\npublic:\n    string MSSV;\n    string tenTruong;\n    string lop;\n    string chuyenNganh;\n};\n```\n\n# Truy xuất thành phần từ lớp cơ sở\n\nKhi class con kế thừa thành phần từ class cơ sở, các thành phần của nó cũng sẽ được thừa hưởng từ class cơ sở. Điều này có nghĩa là, bạn có thể sử dụng con trỏ this để thao tác với thành phần của class hiện tại như bình thường. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    int count;\n \n    void sayHello()\n    {\n        cout << \"Hello, welcome to KhiemLe.Dev!\";\n    }\n};\n \nclass B : public A\n{\npublic:\n    B() // constructor\n    {\n        this->count = 0; // thuộc tính count được kế thừa và có thể truy cập qua con trỏ this\n        this->sayHello(); // phương thức cũng được kế thừa\n    }\n};\n```\n\n# Override phương thức từ lớp cơ sở\n\nTrong khi sử dụng tính kế thừa, bạn sẽ gặp phải trường hợp hai phương thức ở class con và class cha trùng tên nhau. Trong trường hợp này, phương thức được khai báo và định nghĩa ở class con sẽ ghi đè lên phương thức ở class cha và thay thế hoàn toàn nó. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Hello, welcome to khiemle.dev!\";\n    }\n};\n \nclass B : public A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Chao mung ban den voi trang web cua Khiem Le!\";\n    }\n};\n \n// trong hàm main\nB b;\nb.sayHello(); // Chao mung ban den voi trang web cua Khiem Le!\n```\n\nVậy thì bạn có thể thấy được là, nếu như mà phương thức bị trùng tên thì nó bị ghi đè, vậy thì không còn đảm bảo được tính tái sử dụng code của tính kế thừa. Vậy làm sao chúng ta có thể tái sử dụng lại đoạn code của phương thức ở class cha?\n\nSử dụng toán tử phân giải phạm vi (::), chúng ta có thể gọi được phương thức từ class cha, ngoài ra chúng ta còn có thể bổ sung được những đoạn code của riêng phương thức ở class con. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Hello, welcome to khiemle.dev!\";\n    }\n};\n \nclass B : public A\n{\npublic:\n    void sayHello()\n    {\n        A::sayHello(); // Gọi lại phương thức ở class A\n        // Thêm các đoạn code các bạn muốn\n        cout << endl;\n        cout << \"Nho like va share nha ^^!\";\n    }\n};\n \n// trong hàm main\nB b;\nb.sayHello();\n// Hello, welcome to khiemle.dev!\n// Nho like va share nha ^^!\n```\n\nThật đơn giản đúng không nào!\n\n# Upcasting và downcasting\n\nNhiều bạn có thể sẽ thắc mắc, ví dụ “sinh viên là một con người”, vậy thì con trỏ kiểu sinh viên có thể trỏ đến con trỏ kiểu con người hoặc ngược lại hay không. Đây chính là vấn đề upcasting và downcasting. Tuy nhiên, để đảm bảo tính toàn vẹn dữ liệu, bạn chỉ nên xem tham khảo chứ không nên dùng, do có thể gây mất mát dữ liệu hoặc nhận dữ liệu không mong muốn.\n\n## Upcasting\n\nUpcasting là chuyển đổi đối tượng kiểu class con sang kiểu class cha. Tức là, con trỏ kiểu class cha có thể trỏ đến con trỏ kiểu class con, nhưng không bảo toàn được những thuộc tính, phương thức của class con. Việc chuyển kiểu này được thực hiện ngầm định bởi trình biên dịch C++. Các phương thức bị override sẽ bị override ngược lại bởi class cha. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Hello, welcome to khiemle.dev!\";\n    }\n};\n \nclass B : public A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Nho like va share nha ^^!\";\n    }\n \n    void sayGoodbye()\n    {\n        cout << \"Goodbye!\";\n    }\n};\n \n// Trong hàm main\nA *p = new B;\np->sayHello(); // Hello, welcome to khiemle.dev!\np->sayGoodbye(); // Lỗi do A không có phương thức này\n```\n\nRõ ràng, nếu như sinh viên là một con người, thì con trỏ kiểu con người có thể lưu trữ được sinh viên đúng không bởi vì sinh viên là con người mà!\n\nTuy nhiên, do class cha là class tổng quát hóa, có ít thông tin hơn so với class con, do đó, khi chuyển kiểu ta không thể đảm bảo sự toàn vẹn dữ liệu được.\n\n## Downcasting\n\nDowncasting thì ngược lại với upcasting, tức là chuyển đối tượng kiểu class cha sang đối tượng kiểu class con. Cách này lại không được trình biên dịch thực hiện tự động mà bạn phải tự chuyển kiểu nó. Tuy nhiên, các thuộc tính, phương thức ở class con nhiều hơn class cha, do class con là cụ thể hóa của class cha. Do đó, các thuộc tính không có ở class cha sẽ nhận giá trị rác không mong muốn. Ví dụ:\n\n```cpp\nclass A\n{\npublic:\n    void sayHello()\n    {\n        cout << \"Hello, welcome to khiemle.dev!\";\n    }\n};\n \nclass B : public A\n{\npublic:\n    int propInB;\n \n    B()\n    {\n        this->propInB = 0;\n    }\n \n    void sayHello()\n    {\n        A::sayHello();\n        cout << endl;\n        cout << \"Nho like va share nha ^^!\";\n    }\n};\n \n// trong hàm main\nB *p = (B *)new A;\ncout << p->propInB; // Một số ngẫu nhiên\n```\n\n# Đa kế thừa\n\nĐa kế thừa nghĩa là thay vì một class dẫn xuất kế thừa từ một class cơ sở, class dẫn xuất này có thể có kế thừa từ nhiều class cơ sở khác nhau. Ví dụ như một loài động vật thì sẽ kế thừa đặc điểm từ cả bố và mẹ của chúng.\n\nTrong C++, để thực hiện đa kế thừa, cú pháp tương tự như kế thừa, nhưng class cơ sở có thể có nhiều hơn 1, ngăn cách nhau bởi dấu phẩy (,). Ví dụ:\n\n```cpp\nclass A {\npublic:\n    A() {}\n};\nclass B {\npublic:\n    B() {}\n};\n \n// Class C kế thừa từ class A và B\nclass C : public A, public B {\npublic:\n    C() {}\n};\n```\n\nKhi thừa kế từ nhiều class cơ sở, bạn thấy sẽ có một vấn đề phát sinh chính là khi lớp cơ sở có phương thức trùng tên, khi gọi phương thức sẽ xảy ra sự mơ hồ. Sự mơ hồ nghĩa là trình biên dịch không biết bạn đang gọi phương thức từ class dẫn xuất nào. Ví dụ:\n\n```cpp\nclass A {\npublic:\n    A() {}\n \n    void sayHello() { cout << \"Hello in class A\"; }\n};\nclass B {\npublic:\n    B() {}\n \n    void sayHello() { cout << \"Hello in class B\"; }\n};\n \n// Lớp C kế thừa từ lớp A và B\nclass C : public A, public B {\npublic:\n    C() {}\n \n    void sayHello() {\n        sayHello(); // Báo lỗi do mơ hồ, không biết sayHello ở class A hay B\n    }\n};\n```\n\nĐể giải quyết sự nhập nhằn này, bạn có thể sử dụng toán tử phạm vi (::) để chỉ rõ phương thức ở class nào như sau:\n\n```cpp\nclass A {\npublic:\n    A() {}\n \n    void sayHello() { cout << \"Hello in class A\"; }\n};\nclass B {\npublic:\n    B() {}\n \n    void sayHello() { cout << \"Hello in class B\"; }\n};\n \n// Lớp C kế thừa từ lớp A và B\nclass C : public A, public B {\npublic:\n    C() {}\n \n    void sayHello() {\n        A::sayHello(); // Gọi lại phương thức sayHello ở class A\n    }\n};\n```\n\n# Tổng kết\n\nTrong phần kế thừa này, cơ bản nó không khó, chỉ cần bạn phân tích đặc tả được dữ liệu mà đề bài cho, hoặc trong tình huống thực tế, bạn sẽ làm được nó. Trong thời gian tới mình sẽ cố gắng tổng hợp bài tập về phần này để các bạn nắm được về kế thừa trong C++.\n\nVậy là qua bài này, mình đã giới thiệu cho các bạn biết về kế thừa trong C++ nói riêng và trong lập trình hướng đối tượng nói chung. Hi vọng là các bạn thấy thấy bài viết của mình, đừng quên chia sẻ cho bạn bè cùng biết nha.\n\nNếu có bất kỳ thắc mắc hoặc góp ý nào, các bạn có thể comment phía bên dưới bài viết. Cảm ơn các bạn đã đọc bài viết!\n\n# Tham khảo\n\n1. [Cơ bản về class trong C++](/blog/co-ban-ve-class-trong-cpp)\n1. [Vector trong C++](/blog/vector-trong-cpp)","tags":["Lập trình","C++"]},{"slug":"cac-thuat-toan-tim-kiem-pho-bien","title":"Các thuật toán tìm kiếm phổ biến","date":"2020-05-04","topic":"tutorial","summary":"Trong thực tế, có rất nhiều bài toán, nhưng hầu như tất cả chúng đều quy về một bài toán duy nhất, đó chính là bài toán tìm kiếm. Ví dụ như khi bạn giải một bài toán, bạn có làm cách nào đi nữa thì mục đích cuối cùng của bạn chính là đi tìm lời giải…","body":"![Các thuật toán tìm kiếm phổ biến](/api/uploads/17245d9fde2f716c1402.jpg)\n\n# Vì sao cần có thuật toán tìm kiếm?\n\nTrong thực tế, có rất nhiều bài toán, nhưng hầu như tất cả chúng đều quy về một bài toán duy nhất, đó chính là bài toán tìm kiếm. Ví dụ như khi bạn giải một bài toán, bạn có làm cách nào đi nữa thì mục đích cuối cùng của bạn chính là đi tìm lời giải của bài toán. Hay khi bạn thực hiện sắp xếp, lọc các phần tử của danh sách, mục đích của bạn cũng là tìm kiếm những phần tử thỏa mãn yêu cầu.\n\nTừ những nhu cầu thực tế đó, bài toán tìm kiếm dẫn đến chúng ta phải tạo ra thuật toán tìm kiếm để giải quyết nó. Vậy thì thuật toán tìm kiếm là gì? Thuật toán tìm kiếm (searching algorithm) là thuật toán giúp ta tìm ra trong một tập dữ liệu đã cho một hoặc nhiều phần tử thỏa mãn yêu cầu tìm kiếm.\n\nTùy theo cấu trúc dữ liệu mà chúng ta sẽ có những thuật toán tìm kiếm khác nhau phù hợp cho mỗi cấu trúc đó. Do đó, chúng ta không nên học thuộc lòng thuật toán tìm kiếm trên một tập dữ liệu, một cấu trúc dữ liệu trong một ngôn ngữ cụ thể nào đó. Hãy học ý tưởng của thuật toán và áp dụng nó linh hoạt cho các cấu trúc dữ liệu khác nhau trong các ngôn ngữ lập trình khác nhau.\n\nTrong bài này, mình sẽ dùng C++ để minh họa cho các thuật toán tìm kiếm, các bạn có thể áp dụng nó cho bất kỳ ngôn ngữ lập trình nào mà các bạn thích như Java hay Python…\n\n# Các thuật toán tìm kiếm\n\nTrong bài viết này, mình sẽ giới thiệu đến các bạn 3 thuật toán tìm kiếm phổ biến nhất: tìm kiếm tuyến tính, tìm kiếm nhị phân và tìm kiếm nội suy. Giờ hãy bắt đầu tìm hiểu về các thuật toán tìm kiếm này nhé!\n\n## Tìm kiếm tuyến tính\n\nTìm kiếm tuyến tính (linear search) hay tìm kiếm tuần tự (sequential search) là thuật toán tìm kiếm bằng cách duyệt qua tất cả các phần tử của danh sách cho đến khi gặp phần tử cần tìm hoặc là đã hết danh sách. Do cách tìm kiếm duyệt từ đầu đến cuối này, độ phức tạp thời gian của thuật toán này sẽ là O(n).\n\nChúng ta có một mảng A có n phần tử bắt đầu từ vị trí 0. Để tìm kiếm phần tử x trong mảng A này, ta làm như sau:\n\n1.  Gán i = 0.\n2.  So sánh giá trị của A\\[i\\] và x:\n    -   Nếu A\\[i\\] == x thì dừng và trả về giá trị của i (vị trí của x trong mảng A).\n    -   Nếu A\\[i\\] != x thì sang bước 3.\n3.  Gán i = i + 1:\n    -   Nếu i == n (tức hết mảng) thì dừng lại và trả kết quả là -1 (không tìm thấy x).\n    -   Nếu i < n thì quay lại bước 2.\n\n![Tìm kiếm tuyến tình](/api/uploads/a2acb3b4f6fc2ced42e1.png)\n\nTìm kiếm tuyến tính\n\nDựa trên những thao tác trên, chúng ta có thể viết lại code trong C++ như sau:\n\n```cpp\nint LinearSearch(int A[], int n, int x)\n{\n    int i = 0;\n    while (i < n && A[i] != x)\n        i++;\n    if (i == n)\n        return -1; // không tìm thấy x\n    return i;  // tìm thấy x, trả về vị trí của x trong mảng a\n}\n```\n\nThông thường, các bạn hay sử dụng for cho nó đơn giản như sau:\n\n```cpp\nint LinearSearch(int A[], int n, int x)\n{\n    for (int i = 0; i < n; i++)\n        if (A[i] == x)\n            return i;\n    return -1; // duyệt hết mảng, không tìm thấy x\n}\n```\n\nChúng ta có thể cải tiến nó một chút bằng phương pháp đặt lính canh như sau: gán A\\[n\\] = x, trong lúc kiểm tra không cần kiểm tra i < n nữa bởi vì nếu chạy đến cuối cùng thế nào cũng gặp x chính là “lính” chúng ta vừa đặt. Vậy thuật toán trở thành:\n\n```cpp\nint LinearSearch(int A[], int n, int x)\n{\n    int i = 0;\n    A[n] = x;\n    while (A[i] != x)\n        i++;\n    if (i == n)\n        return -1;\n    return i;\n}\n```\n\nCác bạn có thể xem video sau để hiểu rõ hơn:\n\n[Youtube: Thuật toán tìm kiếm tuyến tính | Khiêm Lê\n\n](https://youtu.be/EBkhFGoEdsM)\n\n## Tìm kiếm nhị phân\n\nTìm kiếm nhị phân (binary search) hay còn một số tên gọi khác nữa như tìm kiếm nửa khoảng (half-interval search), tìm kiếm logarit (logarithmic search), chặt nhị phân (binary chop) là thuật toán tìm kiếm dựa trên việc chia đôi khoảng đang xét sau mỗi lần lặp, sau đó xét tiếp trong nửa khoảng có khả năng chứa giá trị cần tìm, cứ như vậy cho đến khi không chia đôi khoảng được nữa. Thuật toán tìm kiếm nhị phân **chỉ áp dụng được cho danh sách đã có thứ tự hay đã được sắp xếp.**\n\nVí dụ như bạn có một dãy số tăng từ 1 đến 100, yêu cầu bạn tìm số 30. Bạn xem phần tử chính giữa của dãy số thì thấy là số 50, vậy thì bạn biết chắc là 30 chỉ có thể nằm trong khoảng dưới 50 thôi, vậy thì giới hạn tìm kiếm được thu hẹp lại một nửa. Ví dụ như tìm số 70 chẳng hạn thì 50 lại nhỏ hơn 70, do đó ta biết chắc 70 chỉ có thể nằm trong khoảng từ 51 đến 100 thôi. Cứ tiếp tục như thế cho đến khi tìm gặp hoặc không thể chia đôi khoảng nữa.\n\nDo cách tìm kiếm chia đôi khoảng này, sau mỗi lần lặp, khoảng đang xét lại được chia đôi, và tiếp tục khoảng tiếp lại chia đôi khoảng đã được chia trước đó. Do đó, độ phức tạp thời gian của thuật toán này sẽ là O(log(n)), tốt hơn rất rất nhiều so với tìm kiếm tuyến tính.\n\nCho một mảng A có n phần tử bắt đầu từ vị trí 0, mảng A được **sắp xếp tăng dần** (lưu ý là thứ tự tăng dần, đối với giảm dần chúng ta sẽ có cách cài đặt khác một chút sẽ được trình bày bên dưới). Để tìm phần tử có giá trị x trong mảng A chúng ta sẽ cài đặt thuật toán tìm kiếm nhị phân như sau:\n\n1.  Gán `left = 0`, `right = n - 1`.\n2.  Gán `mid = (left + right) / 2` (lấy phần nguyên, đây là phần tử chính giữa của khoảng hiện tại)\n    -   Nếu như A\\[mid\\] == x:\n        -   Dừng lại và trả về giá trị của mid (chính là vị trí của x trong mảng A).\n    -   Nếu như A\\[mid\\] > x (có thể x nằm trong nửa khoảng trước):\n        -   right = mid - 1 // giới hạn khoảng tìm kiếm lại là nửa khoảng trước\n    -   Nếu như A\\[mid\\] < x (có thể x nằm trong nửa khoảng sau):\n        -   left = mid + 1 // giới hạn khoảng tìm kiếm lại là nửa khoảng sau\n3.  Nếu `left <= right`:\n    -   Đúng thì quay lại bước 2 (còn chia đôi được).\n    -   Sai thì dừng và trả về kết quả -1 (không tìm thấy x)\n\n![Tìm kiếm nhị phân](/api/uploads/95221563e03d18676944.png)\n\nTìm kiếm nhị phân\n\nThuật toán đã có, giờ hãy xem cài đặt nó trong C++ như thế nào nha:\n\n```cpp\nint BinarySearch(int A[], int n, int x)\n{\n    int left = 0;\n    int right = n - 1;\n    int mid;\n    while (left <= right)\n    {\n        mid = (left + right) / 2;\n        if (A[mid] == x)\n            return mid; // tìm thấy x, trả về mid là vị trí của x trong mảng A\n        if (A[mid] > x)\n            right = mid - 1; // Giới hạn khoảng tìm kiếm lại là nửa khoảng trước\n        else if (A[mid] < x)\n            left = mid + 1; // Giới hạn khoảng tìm kiếm lại là nửa khoảng sau\n    }\n    return -1; // không tìm thấy x\n}\n```\n\nĐối với mảng được sắp xếp giảm, các bạn chỉ cần thay đổi chỗ so sánh A\\[mid\\] và x như sau:\n\n-   A\\[mid\\] < x:\n    -   right = mid - 1\n-   A\\[mid\\] > x:\n    -   left = mid + 1\n\nVideo minh họa ý tưởng thuật toán:\n\n[Youtube: Thuật toán tìm kiếm nhị phân | Khiêm Lê\n\n](https://youtu.be/yT6wluz_A8k)\n\n## Tìm kiếm nội suy\n\nTìm kiếm nội suy (interpolation search) là một thuật toán cải tiến từ thuật toán tìm kiếm nhị phân. Thay vì xác định điểm chính giữa của danh sách, thuật toán tìm kiếm nội suy xác định điểm gần với vị trí của phần tử cần tìm, do đó tối ưu được thời gian hơn so với thuật toán tìm kiếm nhị phân. Độ phức tạp thời gian cũng vì thế mà tốt hơn là O(log(log(n))).\n\nTuy nhiên, thuật toán tìm kiếm nhị phân luôn ổn định với độ phức tạp thời gian là O(log(n)), thuật toán tìm kiếm nội suy lại không như vậy. Trong những trường hợp xấu nhất như dãy tăng/giảm phân bố không đều, thuật toán tìm kiếm này đạt độ phức tạp là O(n), không khác gì dùng thuật toán tìm kiếm tuyến tính cả. Do đó, bạn nên sử dụng thuật toán tìm kiếm nhị phân để đảm bảo được độ phức tạp O(log(n)).\n\nVẫn là mảng A, vẫn n phần tử bắt đầu từ 0 và tăng dần. Tìm x trong mảng này dùng thuật toán tìm kiếm nội suy như sau:\n\n1.  Gán `left = 0`, `right = n - 1`.\n2.  Gán mid = left + (right - left) \\* (x - a\\[left\\]) / (a\\[right\\] - a\\[left\\]):\n    -   Nếu như A\\[mid\\] == x:\n        -   Dừng lại và trả về giá trị của mid.\n    -   Nếu như A\\[mid\\] > x:\n        -   right = mid - 1\n    -   Nếu như A\\[mid\\] < x:\n        -   left = mid + 1\n3.  Nếu `left <= right` và x >= A\\[left\\] và x ⇐ A\\[right\\] (x còn nằm trong đoạn \\[A\\[left\\]; A\\[right\\]\\]):\n    -   Đúng thì quay lại bước 2.\n    -   Sai thì dừng và trả về kết quả -1 (không tìm thấy x)\n\nVà tương tự với mảng giảm dần, bạn chỉ cần sửa lại:\n\n-   A\\[mid\\] < x:\n    -   right = mid - 1\n-   A\\[mid\\] > x:\n    -   left = mid + 1\n\nGiờ hãy cùng xem cài đặt thuật toán tìm kiếm nội suy trong C++ với mảng tăng nha:\n\n```cpp\nint InterpolationSearch(int A[], int n, int x)\n{\n    int left = 0;\n    int right = n - 1;\n    int mid;\n    while (left <= right && x >= A[left] && x <= A[right])\n    {\n        mid = left + (right - left) * (x - A[left]) / (A[right] - A[left]);\n        if (A[mid] == x)\n            return mid;\n        if (A[mid] > x)\n            right = mid - 1;\n        else if (A[mid] < x)\n            left = mid + 1;\n    }\n    return -1; // Không tìm thấy x\n}\n```\n\nLưu ý: khi sử dụng thuật toán tìm kiếm nhị phân hoặc nội suy, nếu như mảng chưa được sắp xếp, nên sử dụng kèm với các thuật toán sắp xếp có hiệu suất cao như Quick Sort hay Merge Sort để sắp xếp nhằm tối ưu hóa thuật toán. Nếu tìm kiếm nhanh mà sắp xếp chậm cũng không có ý nghĩa gì đối với tập dữ liệu lớn.\n\n# Tổng kết\n\nVậy là qua bài viết này, mình đã giới thiệu đến các bạn 3 thuật toán tìm kiếm phổ biến nhất mà các lập trình viên nên biết. Trong hầu hết các bài tập, các bạn sẽ cần dùng đến các thuật toán tìm kiếm này, đừng quên luyện tập thật nhiều để thành thục các thao tác trên các cấu trúc dữ liệu khác nhau nha.\n\nNếu như các bạn thấy hay, đừng quên chia sẻ cho bạn bè cùng biết nha. Bạn cũng có thể để lại bình luận bên dưới bài viết nếu có bất kỳ thắc mắc hoặc góp ý nào. Cảm ơn các bạn đã theo dõi bài viết!","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"ngan-xep-va-hang-doi","title":"Ngăn xếp và hàng đợi","date":"2020-04-15","topic":"tutorial","summary":"Ngăn xếp và hàng đợi là hai kiểu cấu trúc dữ liệu động được sử dụng khá phổ biến trong lập trình. Hai kiểu cấu trúc này đều được xây dựng dựa trên danh sách liên kết đơn cho nên khá dễ để cài đặt. Hãy cùng tìm hiểu xem ngăn xếp và hàng đợi là gì và…","body":"![Ngăn xếp và hàng đợi](/api/uploads/39c6bb1cb99edcd1519e.jpg)\n\n# Giới thiệu\n\nNgăn xếp và hàng đợi là hai kiểu cấu trúc dữ liệu động được sử dụng khá phổ biến trong lập trình. Hai kiểu cấu trúc này đều được xây dựng dựa trên danh sách liên kết đơn cho nên khá dễ để cài đặt. Hãy cùng tìm hiểu xem ngăn xếp và hàng đợi là gì và cách cài đặt như thế nào nhé!\n\nĐể đọc hiểu bài này tốt nhất, các bạn nên có kiến thức về danh sách liên kết đơn, các thao tác trên danh sách đó. Nếu bạn nào chưa biết, bạn có thể xem lại bài viết *Danh sách liên kết đơn*[\\[1\\]](#tham-khao) của mình.\n\n# Ngăn xếp\n\n[Youtube: Ngăn xếp | Khiêm Lê\n\n](https://youtu.be/w0Jbe1uCWUc)\n\nNgăn xếp hay Stack là một dạng danh sách liên kết hoạt động theo cơ chế **LIFO** (**L**ast **I**n, **F**irst **O**ut), nghĩa là các phần tử được thêm vào sau cùng thì sẽ là phần tử được lấy ra đầu tiên.\n\n## Các thao tác cơ bản trên ngăn xếp\n\nCác thao tác cơ bản trên ngăn xếp như sau:\n\n-   IsEmpty: kiểm tra xem ngăn xếp có rỗng hay không\n-   Push: thêm phần tử vào trên cùng ngăn xếp\n-   Pop: lấy phần tử nằm trên cùng ra khỏi ngăn xếp và trả về giá trị của phần tử đó (nếu ngăn xếp không rỗng)\n-   Top: trả về giá trị của phần tử nằm trên cùng của ngăn xếp (nếu ngăn xếp không rỗng)\n\n![Ngăn xếp](/api/uploads/ede15c925c0326d30a0c.png)\n\nNgăn xếp\n\n## Cài đặt ngăn xếp\n\nBây giờ chúng ta hãy xem cách cài đặt ngăn xếp trong C++ như thế nào.\n\n### Cấu trúc một phần tử\n\nTương tự với danh sách liên kết, phần tử trong ngăn xếp cũng có thành phần dữ liệu và thành phần liên kết. Để đơn giản, mình sẽ sử dụng kiểu int cho phần dữ liệu. Phần tử thêm vào đầu sẽ liên kết với phần tử phía dưới của stack qua thành phần liên kết là một con trỏ.\n\n```cpp\nstruct Node\n{\n    int data;\n    Node *next;\n};\n```\n\nVậy để tạo một phần tử, chúng ta cấp phát động một phần tử, gán thành phần dữ liệu của phần tử đó bằng đối số truyền vào, phần tử mới chưa trỏ vào node nào nên phần liên kết gán bằng NULL và trả về địa chỉ của phần tử đó.\n\n```cpp\nNode *CreateNode(int init)\n{\n    Node *node = new Node;\n    node->data = init;\n    node->next = NULL;\n    return node;\n}\n```\n\n### Cấu trúc một ngăn xếp\n\nCác phần tử trong ngăn xếp được xếp vào trên cùng, lúc lấy ra phần tử trên cùng cũng sẽ được lấy ra trước. Do đó, chúng ta chỉ cần nắm được phần tử trên cùng của stack là được. Mình sẽ tạo một struct có thành phần head là một con trỏ lưu địa chỉ của phần tử trên cùng của stack (head).\n\n```cpp\nstruct Stack\n{\n    Node *head;\n};\n```\n\nMột stack mới khởi tạo đương nhiên sẽ không có phần tử nào, chúng ta sẽ khởi gán giá trị NULL cho head của stack đó.\n\n```cpp\nvoid CreateStack(Stack &s)\n{\n    s.head = NULL;\n}\n```\n\n### Kiểm tra ngăn xếp rỗng\n\nĐể kiểm tra ngăn xếp rỗng (IsEmpty), đơn giản ta chỉ cần kiểm tra xem có phần tử trên cùng của stack hay không, nếu không có thì stack đó chắc chắn rỗng.\n\n```cpp\nint IsEmpty(Stack s)\n{\n    if (s.head == NULL)\n        return 1;\n    return 0;\n}\n```\n\n### Thêm một phần tử vào ngăn xếp\n\nĐể thêm một phần tử vào ngăn xếp (Push), chúng ta sẽ thêm nó vào trên cùng của stack tương tự như thêm vào đầu danh sách liên kết đơn.\n\n```cpp\nvoid Push(Stack &s, Node *node)\n{\n    if (s.head == NULL)\n        s.head = node;\n    else\n    {\n        node->next = s.head;\n        s.head = node;\n    }\n}\n```\n\n### Lấy phần tử trên cùng ra khỏi ngăn xếp\n\nĐể lấy phần tử trên cùng ra khỏi ngăn xếp (Pop), chúng ta chỉ cần lưu trữ giá trị của phần tử trên cùng, và xóa nó đi tương tự như xóa phần tử đầu danh sách liên kết đơn. Đương nhiên chúng ta phải kiểm tra xem ngăn xếp đó không rỗng thì mới thực hiện, không thì trả về kết quả không pop được.\n\n```cpp\nint Pop(Stack &s)\n{\n    if (IsEmpty(s))\n        return 0;\n    Node *node = s.head;\n    int data = node->data; // lưu trữ lại giá trị của node\n    s.head = node->next;\n    delete node;   // hủy node\n    return data;\n}\n```\n\n### Lấy giá trị phần tử trên cùng ngăn xếp\n\nĐể lấy giá trị phần tử nằm trên cùng ngăn xếp (Top), chúng ta chỉ cần làm tương tự như lấy phần tử trên cùng ra khỏi ngăn xếp, nhưng chúng ta không hủy node đó đi.\n\n```cpp\nint Top(Stack s)\n{\n    if (IsEmpty(s))\n        return 0;\n    return s.head->data;\n}\n```\n\nLưu ý: cần phân biệt giữa Pop và Top. Pop là lấy phần tử trên cùng ra khỏi ngăn xếp tức là trả về giá trị phần tử đó và hủy đi. Top chỉ trả về giá trị phần tử trên cùng mà không hủy phần tử đó.\n\n# Hàng đợi\n\n[Youtube: Hàng đợi | Khiêm Lê\n\n](https://youtu.be/EQsJxWX6p8k)\n\nHàng đợi hay queue là một cấu trúc dữ liệu động hoạt động theo cơ chế **FIFO** (**F**irst **I**n, **F**irst **O**ut), nghĩa là phần tử được thêm vào đầu tiên sẽ là phần tử được lấy ra đầu tiên.\n\n## Các thao tác cơ bản trên hàng đợi\n\nCác thao tác cơ bản trên hàng đợi:\n\n-   IsEmpty: kiểm tra hàng đợi có rỗng hay không\n-   EnQueue: thêm một phần tử vào cuối hàng đợi\n-   DeQueue: lấy phần tử đầu tiên ra khỏi hàng đợi (nếu hàng đợi không rỗng)\n-   Front: trả về giá trị của phần tử ở đầu hàng đợi (nếu hàng đợi không rỗng)\n\n![Hàng đợi](/api/uploads/12f548581da1d3ac4f03.png)\n\nHàng đợi\n\n## Cài đặt hàng đợi\n\nBây giờ chúng ta hãy cùng xem cách cài đặt hàng đợi trong C++.\n\n### Cấu trúc một phần tử\n\nHàng đợi cũng dựa trên danh sách liên kết đơn, do đó một phần tử trong hàng đợi có cấu trúc không khác gì một phần tử trong danh sách liên kết đơn.\n\n```cpp\nstruct Node\n{\n    int data;\n    Node *next;\n};\n```\n\nCấp phát động, khởi gán giá trị một node và trả về địa chỉ của node đó tương tự như stack hay linked list:\n\n```cpp\nNode* CreateNode(int init)\n{\n    Node *node = new Node;\n    node->data = init;\n    node->next = NULL;\n    return node;\n}\n```\n\n### Cấu trúc một hàng đợi\n\nKhông giống như ngăn xếp, hàng đợi yêu cầu ta phải quản lý được cả phần tử đầu và cuối, do chúng ta thêm vào hàng đợi là thêm vào cuối và lấy một phần tử là lấy từ đầu hàng đợi. Vậy chúng ta sẽ có cấu trúc Queue như sau:\n\n```cpp\nstruct Queue\n{\n    Node *head;\n    Node *tail;\n};\n```\n\nTương tự, hàng đợi rỗng khi được khởi tạo, ta sẽ gán head và tail bằng NULL:\n\n```cpp\nvoid CreateQueue(Queue &q)\n{\n    q.head = NULL;\n    q.tail = NULL;\n}\n```\n\n### Kiểm tra hàng đợi rỗng\n\nTương tự như stack, hàng đợi rỗng khi phần tử đầu hàng đợi bằng NULL, chúng ta sẽ kiểm tra như sau\n\n```cpp\nint IsEmpty(Queue q)\n{\n    if (q.head == NULL)\n        return 1;\n    return 0;\n}\n```\n\n### Thêm phần tử vào cuối hàng đợi\n\nThêm phần tử vào cuối hàng đợi (EnQueue) thực hiện cũng tương tự như khi ta thêm phần tử vào cuối danh sách liên kết đơn, tức là thêm vào tail. Chúng ta thực hiện như sau:\n\n```cpp\nvoid EnQueue(Queue &q, Node *node)\n{\n    if (IsEmpty(q))\n    {\n        q.head = node;\n        q.tail = node;\n    }\n    else\n    {\n        q.tail->next = node;\n        q.tail = node;\n    }\n}\n```\n\n### Lấy phần tử đầu ra khỏi hàng đợi\n\nĐể lấy phần tử đầu ra khỏi hàng đợi (DeQueue), chúng ta sẽ lưu trữ giá trị phần tử đầu hàng đợi, sau đó xóa nó đi như xóa phần tử đầu của danh sách liên kết đơn, tất nhiên là với điều kiện hàng đợi không rỗng. Sau khi lấy phần tử đầu tiên ra, nếu như đó là phần tử duy nhất của hàng đợi thì chúng ta sẽ gán lại tail bằng NULL luôn. Chúng ta sẽ có đoạn code như sau:\n\n```cpp\nint DeQueue(Queue &q)\n{\n    if (IsEmpty(q))\n        return 0;\n    Node *node = q.head;\n    int data = node->data;\n    q.head = node->next;\n    delete node;\n    if (q.head == NULL)\n        q.tail = NULL;\n    return data;\n}\n```\n\n### Lấy giá trị phần tử đầu hàng đợi\n\nLấy giá trị phần tử đầu hàng đợi cũng tương tự như lấy phần tử đầu ra khỏi hàng đợi, nhưng không xóa phần tử đầu đi. Chúng ta thực hiện như sau:\n\n```cpp\nint Front(Queue q)\n{\n    if (IsEmpty(q))\n        return 0;\n    return q.head->data;\n}\n```\n\n# Tổng kết\n\nVậy là trong bài này, mình đã giới thiệu cho các bạn thêm hai cấu trúc dữ liệu phổ biến đó chính là ngăn xếp (stack) và hàng đợi (queue). Nếu các bạn thấy bài viết này hay, đừng quên chia sẻ cho bạn bè cùng biết, các bạn có thể để lại comment bên dưới nếu có bất kỳ thắc mắc nào. Cảm ơn các bạn đã theo dõi bài viết!\n\n# Source code\n\n```cpp\n#include <iostream>\nusing namespace std;\n \nstruct Node\n{\n    int data;\n    Node *next;\n};\n \nstruct Stack\n{\n    Node *head;\n};\n \nvoid CreateStack(Stack &s)\n{\n    s.head = NULL;\n}\n \nNode *CreateNode(int init)\n{\n    Node *node = new Node;\n    node->data = init;\n    node->next = NULL;\n    return node;\n}\n \nint IsEmpty(Stack s)\n{\n    if (s.head == NULL)\n        return 1;\n    return 0;\n}\n \nvoid Push(Stack &s, Node *node)\n{\n    if (IsEmpty(s))\n        s.head = node;\n    else\n    {\n        node->next = s.head;\n        s.head = node;\n    }\n}\n \nint Pop(Stack &s)\n{\n    if (IsEmpty(s))\n        return 0;\n    Node *node = s.head;\n    int data = node->data;\n    s.head = node->next;\n    delete node;\n    return data;\n}\n \nint Top(Stack s)\n{\n    if (IsEmpty(s))\n        return 0;\n    return s.head->data;\n}\n \nvoid DestroyStack(Stack &s)\n{\n    Node *node = s.head;\n    while (s.head != NULL)\n    {\n        s.head = node->next;\n        delete node;\n        node = s.head;\n    }\n}\n \nvoid PrintStack(Stack s)\n{\n    Node *node = s.head;\n    while (node != NULL)\n    {\n        cout << node->data << ' ';\n        node = node->next;\n    }\n}\n \nint main()\n{\n    Stack stack;\n    CreateStack(stack);\n \n    Node *node;\n    for (int i = 0; i < 10; i++)\n    {\n        node = CreateNode(i + 1);\n        Push(stack, node);\n    }\n    PrintStack(stack); // 10 9 8 7 6 5 4 3 2 1\n    cout << endl;\n \n    cout << Pop(stack) << endl; // 10\n    PrintStack(stack); // 9 8 7 6 5 4 3 2 1\n    cout << endl;\n \n    cout << Top(stack) << endl; // 9\n    PrintStack(stack); // 9 8 7 6 5 4 3 2 1\n \n    DestroyStack(stack);\n \n    return 0;\n}\n```\n\n```cpp\n#include <iostream>\nusing namespace std;\n \nstruct Node\n{\n    int data;\n    Node *next;\n};\n \nstruct Queue\n{\n    Node *head;\n    Node *tail;\n};\n \nvoid CreateQueue(Queue &q)\n{\n    q.head = NULL;\n    q.tail = NULL;\n}\n \nNode* CreateNode(int init)\n{\n    Node *node = new Node;\n    node->data = init;\n    node->next = NULL;\n    return node;\n}\n \nvoid DestroyQueue(Queue &q)\n{\n    Node *node = q.head;\n    while (q.head != NULL)\n    {\n        q.head = node->next;\n        delete node;\n        node = q.head;\n    }\n    q.tail = NULL;\n}\n \nint IsEmpty(Queue q)\n{\n    if (q.head == NULL)\n        return 1;\n    return 0;\n}\n \nvoid EnQueue(Queue &q, Node *node)\n{\n    if (IsEmpty(q))\n    {\n        q.head = node;\n        q.tail = node;\n    }\n    else\n    {\n        q.tail->next = node;\n        q.tail = node;\n    }\n}\n \nint DeQueue(Queue &q)\n{\n    if (IsEmpty(q))\n        return 0;\n    Node *node = q.head;\n    int data = node->data;\n    q.head = node->next;\n    delete node;\n    if (q.head == NULL)\n        q.tail = NULL;\n    return data;\n}\n \nint Front(Queue q)\n{\n    if (IsEmpty(q))\n        return 0;\n    return q.head->data;\n}\n \nvoid PrintQueue(Queue q)\n{\n    Node *node = q.head;\n    while (node != NULL)\n    {\n        cout << node->data << ' ';\n        node = node->next;\n    }\n}\n \nint main()\n{\n \n    Queue queue;\n    CreateQueue(queue);\n \n    Node *node;\n    for (int i = 0; i < 10; i++)\n    {\n        node = CreateNode(i + 1);\n        EnQueue(queue, node);\n    }\n \n    PrintQueue(queue); // 1 2 3 4 5 6 7 8 9 10\n    cout << endl;\n \n    cout << DeQueue(queue) << endl; // 1\n    PrintQueue(queue); // 2 3 4 5 6 7 8 9 10\n    cout << endl;\n \n    cout << Front(queue) << endl; // 2\n    PrintQueue(queue); // 2 3 4 5 6 7 8 9 10\n \n    return 0;\n}\n```\n\n# Tham khảo\n\n1. [Danh sách liên kết đơn](/blog/danh-sach-lien-ket-don)","tags":["Cấu trúc dữ liệu","Lập trình","C++"]},{"slug":"nap-chong-toan-tu-trong-cpp","title":"Nạp chồng toán tử trong C++","date":"2020-04-10","topic":"tutorial","summary":"Nếu bạn đã học qua lập trình C++ cơ bản, chắc chắc rằng trong hầu hết các bài tập về C++, bạn đều sử dụng các toán tử số học như cộng, trừ, nhân, chia. Hầu hết các toán tử đó đều được thực hiện trên toán hạng có kiểu dữ liệu cơ bản như int, float, double…","body":"![Nạp chồng toán tử trong C++](/api/uploads/60c4b70b5ef11db32e6f.jpg)\n\n# Đầu tiên\n\nĐể đọc hiểu bài này tốt nhất, bạn nên có kiến thức về:\n\n-   [Cơ bản về class trong C++](/blog/co-ban-ve-class-trong-cpp)\n-   [Hàm bạn và lớp bạn trong C++](/blog/ham-ban-va-lop-ban-trong-cpp)\n-   Tham chiếu trong C++\n-   Ép kiểu ngầm định và ép kiểu tường minh\n\n# Đặt vấn đề\n\nNếu bạn đã học qua lập trình C++ cơ bản, chắc chắc rằng trong hầu hết các bài tập về C++, bạn đều sử dụng các toán tử số học như cộng, trừ, nhân, chia. Hầu hết các toán tử đó đều được thực hiện trên toán hạng có kiểu dữ liệu cơ bản như int, float, double…\n\n```cpp\nint a = 5;\nint b = 4;\nint c = a + b;  // = 9\n```\n\nVậy nếu như bạn muốn thực hiện các toán tử đó đối với toán hạng có kiểu dữ liệu bạn tự định nghĩa thì làm sao?\n\n```cpp\nPhanSo ps1(1, 2);\nPhanSo ps2(2, 3);\n// Làm sao để có thể cộng hai phân số?\nPhanSo ketQua = ps1 + ps2;\n```\n\nĐây chính là lúc chúng ta sử dụng nạp chồng toán tử. Vậy hãy cùng tìm hiểu xem nạp chồng toán tử là gì và cách nạp chồng toán tử như thế nào.\n\n# Nạp chồng toán tử là gì?\n\nCũng tương tự như nạp chồng hàm (overload function), bạn có thể định nghĩa nhiều hàm có cùng tên, nhưng khác tham số truyền vào, nạp chồng toán tử cũng tương tự.\n\nNạp chồng toán tử (overload operator) là bạn định nghĩa lại toán tử đã có trên kiểu dữ liệu người dùng tự định nghĩa để dể dàng thể hiện các câu lệnh trong chương trình.\n\nVí dụ như bạn định nghĩa phép cộng cho kiểu dữ liệu phân số thì sẽ thực hiện cộng hai phân số rồi trả về một phân số mới. So với việc thực hiện gọi hàm, việc overload toán tử sẽ làm cho câu lệnh ngắn gọn, dễ hiểu hơn.\n\n```cpp\nPhanSo ps1(1, 2);\nPhanSo ps2(2, 3);\nPhanSo ketQua;\n// Dùng hàm\nketQua = ps1.cong(ps2);\n// Dùng Overload operator\nketQua = ps1 + ps2;  // 7/6\n```\n\n# Cơ chế hoạt động\n\nVề bản chất, việc thực hiện các toán tử cũng tương đương với việc gọi hàm, ví dụ:\n\n```cpp\nPhanSo a(1, 2);\nPhanSo b(2, 3);\nPhanSo ketQua = a + b;\n// Tương đương với\nPhanSo ketQua = a.cong(b);\n```\n\nNếu bạn thực hiện toán tử trên hai toán hạng có kiểu dữ liệu cơ bản (float, double, int…), trình biên dịch sẽ tìm xem phiên bản nạp chồng toán tử nào phù hợp với kiểu dữ liệu đó và sử dụng, nếu không có sẽ báo lỗi.\n\nNgược lại nếu là kiểu dữ liệu tự định nghĩa như struct, class, trình biên dịch sẽ tìm xem có phiên bản nạp chồng toán tử nào phù hợp không? Nếu có thì sẽ sử dụng toán tử đó, ngược lại thì sẽ cố gắng chuyển đổi kiểu dữ liệu của các toán hạng đó sang kiểu dữ liệu có sẵn để thực hiện phép toán, không được sẽ báo lỗi.\n\n# Các toán tử có thể được overload\n\n```cpp\n+\t–\t*\t/\t%\t^\t&\t|\n~\t!\t=\t<\t>\t+=\t-=\t*=\n/=\t%=\t^=\t&=\t|=\t<<\t>>\t>>=\n<<=\t==\t!=\t<=\t>=\t&&\t||\t++\n—\t->*\t,\t->\t[]\t()\tnew\tdelete\nnew[]\tdelete[]\n```\n\n*Bảng các toán tử có thể overload* được.\n\nVậy ta chỉ có một số toán tử sau không overload được:\n\n-   Toán tử `.`\n-   Toán tử phạm vi `::`\n-   Toán tử điều kiện `?:`\n-   Toán tử `sizeof`\n\nMột số lưu ý:\n\n-   Các toán tử một ngôi `--, ++` có thể đứng trước hoặc sau toán hạng\n-   Một số toán tử có thể làm toán tử một ngôi hoặc hai ngôi như toán tử `*`\n-   Toán tử chỉ mục `[...]` là toán tử hai ngôi\n-   Các từ khóa new và delete cũng được xem là toán tử nên có thể được overload\n\n# Cú pháp overload\n\nNhư đã giới thiệu, bản chất việc dùng toán tử là lời gọi hàm, do đó chúng ta overload toán tử cũng giống overload hàm, vậy chúng ta sẽ overload hàm nào? Chúng ta sẽ overload hàm có tên là `operator@`, với `@` là toán tử cần overload `+, -, *, /, ...`, kiểu trả về của hàm chính là lớp đó.\n\nCó hai loại là hàm cục bộ (dùng phương thức của lớp) và hàm toàn cục (dùng hàm bạn). Chúng ta sẽ lần lượt tìm hiểu cách overload toán tử bằng cả hai cách.\n\n## Cài đặt với hàm cục bộ\n\nĐối với hàm cục bộ hay còn gọi là phương thức của lớp, số tham số sẽ ít hơn hàm toàn cục một tham số vì tham số đầu tiên mặc định chính là toán hạng đầu tiên. Vậy, đối với toán tử hai ngôi, ta chỉ cần truyền một tham số cho hàm, chính là toán hạng thứ hai. Ví dụ:\n\n```cpp\nclass PhanSo\n{\n    int tu;\n    int mau;\n \npublic:\n    PhanSo() : tu(0), mau(1) {}\n \n    PhanSo operator+(const PhanSo &ps)    // overload toán tử +\n    {\n        PhanSo kq;\n        kq.tu = this->tu * ps.mau + ps.tu * this->mau;\n        kq.mau = this->mau * ps.mau;\n        return kq;\n    }\n};\n```\n\nSau khi overload toán tử, bạn có thể sử dụng nó trên kiểu dữ liệu bạn đã định nghĩa:\n\n```cpp\n    PhanSo ps1(1, 2);\n    PhanSo ps2(2, 3);\n    PhanSo ps3 = ps1 + ps2;  // = 1/2 + 2/3\n```\n\nGiờ chúng ta hãy xem một ví dụ khác, overload toán tử cộng một phân số với một số nguyên.\n\n```cpp\nclass PhanSo\n{\n    // properties & methods\n    PhanSo operator+(const int &i)\n    {\n        PhanSo kq;\n        kq.tu = this->tu + i * this->mau;\n        return kq;\n    }\n}\n \n// Sử dụng\nPhanSo ps1(1, 2);\nPhanSo ps2 = ps1 + 2;   // = 5/2\n```\n\nDo toán tử overload theo cách này là phương thức, được gọi từ một đối tượng, do đó mặc định toán hạng đầu tiên phải là toán hạng có kiểu dữ liệu của lớp đó, điều này cũng có nghĩa là bạn phải đặt toán hạng có kiểu dữ liệu của lớp đó đầu tiên rối mới đến toán hạng tiếp theo. Và đối với các kiểu dữ liệu có sẵn, ta không thể truy cập vào các lớp định nghĩa nên chúng, do đó ta không thể overload operator của chúng được. Vậy để giải quyết điều này thì làm như thế nào? Ta sẽ sử dụng hàm toàn cục.\n\n## Cài đặt với hàm toàn cục\n\nThay vì đối với việc toán hạng đầu tiên luôn phải có kiểu là lớp nào đó, chúng ta sẽ sử dụng hàm bạn để có thể tự do lựa chọn thứ tự của các toán hạng. Ví dụ như bạn muốn `1 + ps1`, `ps1 + 1` đều được chứ không nhất thiết phải là `ps1 + 1` nữa. Chúng ta cài đặt với hàm bạn tương tự như sau:\n\n```cpp\nclass PhanSo\n{\n    // properties & methods\n    friend PhanSo operator+(const PhanSo &ps, const int &i);\n}\n \nPhanSo operator+(const PhanSo &ps, const int &i)\n{\n    PhanSo kq;\n    kq.tu = ps.tu + i * ps.mau;\n    return kq;\n}\n \n// Sử dụng\nPhanSo ps1(1, 2);\nPhanSo ps2 = ps1 + 2;   // = 5/2\n```\n\nKhá là giống với overload toán tử theo cách dùng phương thức phải không? Vậy nếu muốn đổi thứ tự toán hạng thì phải làm sao, đơn giản đổi thứ tự tham số là được:\n\n```cpp\nPhanSo operator+(const int &i, const PhanSo &ps)  // hàm bạn của class PhanSo\n{\n    return ps + i;\n}\n```\n\n# Chuyển kiểu\n\nCó hai loại chuyển kiểu là chuyển kiểu bằng toán tử chuyển kiểu và chuyển kiểu bằng constructor\n\n## Overload toán tử chuyển kiểu\n\nNhư đã trình bày trong phần cơ chế, nếu không tìm thấy phiên bản nạp chồng toán tử nào phù hợp với kiểu dữ liệu của toán hạng, trình biên dịch sẽ chuyển đối toán hạng sang kiểu dữ liệu cơ bản để tính toán. Vậy chúng ta sẽ cần overload toán tử chuyển kiểu để trình biên dịch có thể chuyển kiểu dữ liệu ta định nghĩa sang kiểu dữ liệu cơ bản.\n\nVí dụ như mình muốn chuyển phân số về số thực, mình sẽ overload toán tử chuyển kiểu float:\n\n```cpp\n// Cú pháp\noperator T()\n{\n    return x; // x có kiểu dữ liệu là T\n}\n \n// Ví dụ overload toán tử chuyển kiểu float\nclass PhanSo\n{\n    // properties & methods\n    operator float();\n}\n \nPhanSo::operator float()\n{\n    return (float)this->tu / this->mau;\n}\n```\n\nLúc này, ta sẽ có thể thực hiện chuyển kiểu dữ liệu:\n\n```cpp\nPhanSo ps1(1, 2);\nPhanSo ps2(2, 3);\nfloat a = ps1 + ps2;\ncout << a  << endl;    // ~ 1.67\ncout << (float)ps1;   // = 0.5\n```\n\n## Chuyển kiểu bằng constructor\n\nĐể hạn chế việc phải overload toán tử với các toán hạng có kiểu dữ liệu khác nhau, ta sử dụng chuyển kiểu bằng constructor. Ví dụ:\n\n```cpp\n// constructor\nPhanSo(int t, int m = 1) : tu(t), mau(m) {}\n```\n\nVới constructor được khai báo như trên, khi ta thực hiện cộng một số nguyên với một kiểu phân số, số nguyên sẽ được trình biên dịch chuyển thành kiểu phân số thông qua việc gọi constructor bên trên, với mẫu số là 1 và tử chính là toán hạng ta đang cộng.\n\n```cpp\nPhanSo ps(1, 2);\nPhanSo kq = ps + 3; // = 7/2\n// Có thể hiểu là\nPhanSo kq = ps + PhanSo(3);\n```\n\n## Sự nhập nhằng\n\nSự nhập nhằng xảy ra khi bạn thực hiện chuyển kiểu bằng constructor và chuyển kiểu bằng toán tử chuyển kiểu. Sự nhập nhằng khiến cho trình biên dịch không xác định được nên chuyển kiểu bằng toán tử chuyển kiểu hay constructor, dẫn đến việc mất đi cơ chế chuyển kiểu tự động (ngầm định).\n\n```cpp\nclass PhanSo\n{\n    // properties & methods\n    PhanSo(int t, int m = 1) : tu(t), mau(m) {}\n    operator double();\n}\n \nPhanSo::operator double()\n{\n    return (double)this->tu / this->mau;\n}\n \nPhanSo a(2, 3), b(3, 4), c;\nc = a + b;\nc = 2 + a;  // lỗi do sự nhập nhằng\nc = a + 2.5;  // lỗi do sự nhập nhằng\n```\n\nCách xử lý duy nhất cho việc này là thực hiển chuyển kiểu tường minh, việc này làm mất đi sự tiện lợi của cơ chế chuyển kiểu tự động. Do đó khi thực hiện chuyển kiểu, ta phải hi sinh một trong hai, hoặc là chuyển kiểu bằng constructor, hoặc là overload toán tử chuyển kiểu.\n\nCác phép toán đã có, toán tử chuyển kiểu cũng đã có, vậy bây giờ nhập hay xuất phân số ta vẫn phải tự nhập xuất tử và mẫu à? Chúng ta sẽ overload luôn toán tử `>>` và `<<`, hãy xem thực hiện như thế nào.\n\n# Overload toán tử nhập xuất\n\nMột toán tử nhập xuất sẽ có hai toán hạng, bến trái là istream hoặc ostream, bên phải là toán hạng cần nhập, xuất. Để overload, chúng ta sử dụng hàm toàn cục, có hai tham số, tham số đầu tiên là một tham chiếu đến đối tượng kiểu istream hoặc ostream, tham số thứ hai là một tham chiếu đối tượng cần nhập, xuất, kiểu trả về của hàm chính là tham chiếu đến tham số đầu tiên của hàm (istream hoặc ostream).\n\n## Toán tử nhập\n\nMình sẽ thực hiện overload toán tử nhập cho lớp phân số của mình như sau:\n\n```cpp\nclass PhanSo\n{\n    // properties & methods\n    friend istream &operator>>(istream &in, PhanSo &ps);\n}\n \nistream &operator>>(istream &in, PhanSo &ps)\n{\n    cout << \"Tu: \";\n    in >> ps.tu;\n    cout << \"Mau: \";\n    in >> ps.mau;\n    return in;\n}\n```\n\nNhư vậy toán tử nhập đã được overload cho lớp phân số, bây giờ khi gọi toán tử nhập chúng ta sẽ được kết quả sau:\n\n```cpp\nPhanSo ps;\ncin >> ps;\n// Tu: 1\n// Mau: 2\n```\n\n## Toán tử xuất\n\nĐối với toán tử nhập cũng tương tự, chúng ta cũng thực hiện tương tự như sau:\n\n```cpp\nclass PhanSo\n{\n    // properties & methods\n    friend ostream &operator<<(ostream &out, const PhanSo &ps);\n}\n \nostream &operator<<(ostream &out, const PhanSo &ps)\n{\n    if (ps.tu == 0)\n        out << 0;\n    else if (ps.mau == 1)\n        out << ps.tu;\n    else\n        out << ps.tu << '/' << ps.mau;\n    return out;\n}\n```\n\nBây giờ bạn có thể sử dụng toán tử xuất bình thường như các kiểu dữ liệu cơ bản khác:\n\n```cpp\nPhanSo ps(1, 2);\ncout << ps; // 1/2\n```\n\n# Hạn chế của việc overload toán tử\n\n-   Không thể tạo toán tử mới\n-   Không thể kết hợp các toán tử theo cách mà trước đó không được định nghĩa\n-   Không thay đổi được thứ tự ưu tiên toán tử\n-   Không thể tạo cú pháp mới cho toán tử\n-   Không thể định nghĩa lại một định nghĩa đã có của một toán tử\n\n# Một số ràng buộc của toán tử\n\n-   Hầu hết các toán tử không ràng buộc ý nghĩa, ngoại trừ một số toán tử `=, [], (), ->` thì phải được định nghĩa là hàm thành phần của lớp để toán hạng đầu tiên luôn nằm bên trái\n-   Nếu đã overload toán tử rồi thì phải làm cho đầy đủ. Ví dụ overload `+, -, *, /` thì phải overload luôn cả `+=, -=, *=, /=...`\n-   Luôn tôn trọng ý nghĩa của toán tử gốc (+ thì phải cộng, - phải trừ…)\n-   Cố gắng tái sử dụng mã nguồn một cách tối đa (ví dụ như đảo thứ tự toán hạng như ví dụ ở trên)\n\n# Tổng kết\n\nVậy là trong bài viết này, mình đã giới thiệu cho các bạn cách nạp chồng toán tử trong C++. Nếu bạn thấy hay, hãy chia sẻ cho bạn bè cùng biết, và đừng ngần ngại góp ý dưới bài viết để giúp mình phát triển bài viết tốt hơn. Cảm ơn các bạn đã theo dõi bài viết.","tags":["Lập trình","C++"]},{"slug":"ham-ban-va-lop-ban-trong-cpp","title":"Hàm bạn và lớp bạn trong C++","date":"2020-04-05","topic":"tutorial","summary":"Giả sử bạn có hai class là MyClass1 và MyClass2, không dùng getter và setter, bạn hãy giải quyết hai vấn đề sau:","body":"![Hàm bạn và lớp bạn trong C++](/api/uploads/35a58184d7d552b56bfe.jpg)\n\n# Đặt vấn đề\n\nGiả sử bạn có hai class là MyClass1 và MyClass2, không dùng getter và setter, bạn hãy giải quyết hai vấn đề sau:\n\n-   Truy cập thành phần private hoặc protected của hai class trên trong một hàm khác không phải thành phần của hai class đó.\n-   Truy cập thành phần private hoặc protected của MyClass1 từ thành phần của class MyClass2.\n\nNhìn qua có vẻ khó nhưng nếu bạn biết được hàm bạn và lớp bạn thì mọi chuyện sẽ cực kỳ đơn giản.\n\n# Hàm bạn\n\nHàm bạn (friend function) là hàm có thể truy cập thành phần private hoặc protected của lớp xem nó là bạn.\n\nGiả sử mình có class MyClass và hàm myFunc như sau:\n\n```cpp\nclass MyClass\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass(int n) // constructor\n    {\n        this->_privateProp = n;\n    }\n};\n \nvoid myFunc(MyClass mClass)\n{\n    cout << mClass._privateProp; // Không hợp lệ\n}\n```\n\nNhư bạn thấy trong đoạn code trên, dòng in ra giá trị thuộc tính \\_privateProp là không hợp lệ do nó là thuộc tính private và không được phép truy cập từ bên ngoài. Tuy nhiên mọi chuyện sẽ khác nếu hàm myFunc là hàm bạn của lớp MyClass.\n\nĐể khai báo hàm bạn, ta có cú pháp như sau:\n\n```cpp\nclass <ClassName>\n{\n    // properties & methods\n    friend <return_type> <func_name>([<parameters>]);\n};\n```\n\nChỉ đơn giản như bạn khai báo function prototype và thêm từ khóa friend ở phía trước. Vậy thì đối với ví dụ của mình thì sẽ thêm như sau:\n\n```cpp\nclass MyClass\n{\n    // properties & methods\n    friend void myFunc(MyClass);\n};\n```\n\n**Lưu ý**: nhiều bạn thường nhầm hàm myFunc là thành phần của class MyClass. Hàm myFunc **không phải** là thành phần của lớp MyClass, nó chỉ là một hàm bình thường (không hề có toán tử phạm vi `::` trước tên hàm).\n\nBây giờ thì hàm myFunc đã là hàm bạn của lớp MyClass, do đó đoạn code in giá trị thuộc tính \\_privateProp sẽ hợp lệ. VÍ dụ hoàn chỉnh như sau:\n\n```cpp\n#include <iostream>\nusing namespace std;\n \nclass MyClass\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass(int n)\n    {\n        this->_privateProp = n;\n    }\n \n    friend void myFunc(MyClass);\n};\n \nvoid myFunc(MyClass mClass)\n{\n    cout << mClass._privateProp;\n}\n \nint main()\n{\n    MyClass mClass(2409);\n    myFunc(mClass); // in ra màn hình 2409\n    return 0;\n}\n```\n\nNhư vậy vấn đề đầu tiên đã được giải quyết, giờ chúng ta hãy đến với vấn đề thứ hai.\n\n# Lớp bạn\n\nLớp bạn (friend class) là lớp có thể truy cập các thành phần private hoặc protected của lớp xem nó là bạn.\n\nGiả sử mình có hai class như sau:\n\n```cpp\nclass MyClass1\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass1(int n)\n    {\n        this->_privateProp = n;\n    }\n};\n \nclass MyClass2\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass2(int n) // constructor\n    {\n        this->_privateProp = n;\n    }\n \n    void myMethod(MyClass1 mClass)\n    {\n        cout << mClass._privateProp; // Không hợp lệ\n    }\n};\n```\n\nTrong đoạn code trên, dòng in giá trị thuộc tính \\_privateProp trong phương thức myMethod của lớp MyClass2 không hợp lệ do ta không thể truy cập thành phần private hoặc protected từ bên ngoài class.\n\nĐể phương thức myMethod có thể truy cập được thành phần private, protected của lớp MyClass1, hay tổng quát hơn là để lớp MyClass2 có thể truy cập thành phần private, protected của lớp MyClass1, ta sử dụng cú pháp:\n\n```cpp\nclass <ClassName>\n{\n    // properties & methods\n    friend class <friendClass>;\n}\n```\n\nQuay trở lại với ví dụ, chúng ta chỉ cần thêm như sau để đoạn code hợp lệ:\n\n```cpp\nclass MyClass1\n{\n    // properties & methods\n    friend class MyClass2;\n}\n```\n\nNhư vậy, lớp MyClass2 đã là lớp bạn của lớp MyClass1, vậy phương thức myMethod của lớp MyClass2 sẽ có quyền truy cập các thành phần private, protected của lớp MyClass1 một cách hợp lệ. Và đây là toàn bộ ví dụ:\n\n```cpp\n#include <iostream>\nusing namespace std;\n \nclass MyClass1\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass1(int n)\n    {\n        this->_privateProp = n;\n    }\n \n    friend class MyClass2;\n};\n \nclass MyClass2\n{\nprivate:\n    int _privateProp;\n \npublic:\n    MyClass2(int n)\n    {\n        this->_privateProp = n;\n    }\n \n    void myMethod(MyClass1 mClass)\n    {\n        cout << mClass._privateProp;\n    }\n};\n \nint main()\n{\n    MyClass1 mClass1(2409);\n    MyClass2 mClass2(2001);\n    mClass2.myMethod(mClass1); // in ra màn hình 2409\n    return 0;\n}\n```\n\nNhư vậy, chúng ta đã giải quyết được vấn đề thứ hai ở phần đặt vấn đề đã đưa ra.\n\n**Lưu ý**: lớp bạn là mối quan hệ một chiều, có nghĩa là lớp này có thể xem lớp kia là bạn, nhưng không có nghĩa lớp kia xem lớp này là bạn. Trong ví dụ trên, lớp MyClass1 xem lớp MyClass2 là bạn nhưng không có nghĩa là lớp MyClass2 xem lớp MyClass1 là bạn (nếu muốn, bạn phải khai báo lớp bạn tương tự cho lớp MyClass2).\n\n# Tổng kết\n\nVậy là qua bài viết này, mình đã giới thiệu đến các bạn hàm bạn và lớp bạn trong C++. Nếu các bạn thấy hay, đừng quên đăng ký nhận thông báo mới nhất và chia sẻ để bạn bè cùng biết. Cảm ơn các bạn đã đọc bài viết!","tags":["Lập trình","C++"]},{"slug":"con-tro-va-cap-phat-dong-trong-cpp","title":"Con trỏ và cấp phát động trong C++","date":"2020-03-29","topic":"tutorial","summary":"Con trỏ (pointer) là một khái niệm quan trọng và khó nhất trong C++, nó thường được dùng để đánh giá mức độ thành thạo C++ của bạn. Việc sử dụng thành thạo con trỏ đi cùng với việc thành thạo các thao tác cấp phát động, quản lý bộ nhớ một cách chặt…","body":"![Pointer in C++](/api/uploads/e2133150ddfd7d421889.jpg)\n\n# Con trỏ và cấp phát động trong C++\n\nCon trỏ (pointer) là một khái niệm quan trọng và khó nhất trong C++, nó thường được dùng để đánh giá mức độ thành thạo C++ của bạn. Việc sử dụng thành thạo con trỏ đi cùng với việc thành thạo các thao tác cấp phát động, quản lý bộ nhớ một cách chặt chẽ trong C++.\n\n# Kiến trúc máy tính\n\nĐể hiểu được bài này, chúng ta cần biết được kiến thức cơ bản về bộ nhớ máy tính, cụ thể là RAM. Chúng ta sẽ không tìm hiểu quá sâu mà chỉ ở mức cơ bản, đủ để có thể hiểu được con trỏ hoạt động như thế nào.\n\nRAM (**R**andom **A**ccess **M**emory) là bộ nhớ được dùng để lưu trữ dữ liệu tạm thời để xử lý và dữ liệu sẽ mất đi khi ngưng cấp điện cho RAM. RAM chứa rất nhiều ô nhớ và mỗi ô nhớ có kích thước là 1 byte (1 byte = 8 bit). Các ô nhớ có địa chỉ duy nhất và được đánh số từ 0 trở đi.\n\nKhi trình biên dịch thực hiện biên dịch code, nó sẽ dành riêng một vùng nhớ cho biến được khai báo, liên kết địa chỉ ô nhớ đầu tiên của vùng nhớ đó với tên biến và mỗi khi gọi đến biến đó, nó sẽ tự truy xuất đến vùng nhớ đã được liên kết với tên biến đó. Vùng nhớ của một biến là tập các ô nhớ liền kề nhau. Các biến khác nhau không nhất thiết các vùng nhớ của nó phải liền kề nhau.\n\n![Memory structure](/api/uploads/a097b9314d6fbacf7a21.png)\n\nMemory structure\n\nTùy vào kích thước của kiểu dữ liệu mà trình biên dịch sẽ cấp phát số ô nhớ liền kề khác nhau tương ứng. Ví dụ như kiểu char có kích thước 1 byte thì sẽ cấp cho biến kiểu char 1 ô nhớ, kiểu int có 4 byte thì sẽ cấp cho 4 ô nhớ liền kề nhau và địa chỉ của biến đó là địa chỉ của ô nhớ đầu tiên của vùng nhớ đó. Ví dụ như hình trên thì ta có biến x kiểu int được cấp phát 4 ô nhớ và địa chỉ của biến x chính là địa chỉ của ô nhớ đầu tiên của vùng 4 ô nhớ đó chính là 0x0B.\n\n# Cấp phát tĩnh và cấp phát động\n\nBiến tĩnh hay biến được cấp phát tĩnh là biến được khai báo bằng cú pháp khai báo biến, có tên và được cấp phát một vùng nhớ cố định trước khi sử dụng. Vùng nhớ cố định ở đây nghĩa là vùng nhớ đó luôn tồn tại khi chương trình thực thi, không thể được xóa đi (tức trả lại cho hệ điều hành) hoặc là thay đổi kích thước (đối với mảng), sau khi kết thúc chương trình sẽ tự động trả vùng nhớ đó lại cho hệ điều hành.\n\nChính việc cấp phát vùng nhớ cố định cho biến tĩnh gây chiếm dụng bộ nhớ nếu ta không có nhu cầu sử dụng biến đó nữa, hoặc ta không thể thay đổi kích thước nếu dữ liệu vượt quá kích thước lưu trữ của biến (đối với mảng). Đây chính là lúc chúng ta sử dụng biến động.\n\nBiến động hay biến được cấp phát động là biến thuộc một kiểu dữ liệu đã định nghĩa, không có tên, không được khai báo trong phần khai báo biến. Điều này có nghĩa là biến động là một biến được cấp phát một vùng nhớ trong bộ nhớ RAM, không được liên kết với tên biến do đó nó không có tên, nó chỉ là một vùng nhớ. Việc quản lý biến động được thực hiện qua con trỏ.\n\n# Biến con trỏ\n\nBiến con trỏ hay thường gọi là con trỏ là biến dùng để lưu trữ giá trị là địa chỉ ô nhớ. Nghĩa là bản thân con trỏ là một biến thông thường nhưng mà nó chứa địa chỉ của biến tĩnh hoặc biến động. Như đã trình bày ở trên, biến động không có tên do đó chỉ có thể được quản lý qua con trỏ, do đó, con trỏ thường được dùng để chứa địa chỉ của biến động, lúc này ta nói con trỏ này trỏ đến hoặc con trỏ này tham chiếu đến biến hoặc vùng nhớ đó. Do con trỏ chỉ chứa địa chỉ nên mọi con trỏ đều có kích thước như nhau.\n\nDo con trỏ liên quan đến việc tham chiếu đến địa chỉ của biến, ta phải tìm hiểu các toán tử & và \\*. Toán tử & và \\* là toán tử một ngôi, toán tử & (address-of operator) được đặt trước tên biến và cho biết địa chỉ ô nhớ đầu tiên trong vùng nhớ của biến đó. Toán tử \\* (dereferencing operator hay indirection operator) được đặt trước một địa chỉ để lấy giá trị lưu trữ tại địa chỉ đó. Ví dụ:\n\n```cpp\nint a = 2409; // Giả sử a được cấp phát vùng nhớ có địa chỉ 0x50\ncout << &a; // lấy địa chỉ của biến a tức là 0x50\ncout << *&a; // in ra giá trị được lưu trữ tại địa chỉ của biến a, tức là 2409\n// hay nói cách khác *&a ~ a\n```\n\nĐể tạo một con trỏ, ta sử dụng cú pháp như sau:\n\n```cpp\n<kiểu_dữ_liệu> *<tên_biến_con_trỏ>;\n// Trong đó <kiểu_dữ_liệu> là kiểu dữ liệu của biến mà con trỏ này trỏ tới\n// Ví dụ\nint *ptr_a; // Khai báo con trỏ có tên là ptr_a\n```\n\nNhớ là kiểu con trỏ là kiểu gì thì ta chỉ được trỏ tới biến kiểu đó, ví dụ không thể đem một con trỏ int mà trỏ vào biến kiểu double được. Biến con trỏ không có kiểu riêng mà chỉ phụ thuộc vào đối tượng mà nó trỏ đến, do đó khi chưa xác định được kiểu dữ liệu của đối tượng trỏ đến, ta dùng kiểu void.\n\n```cpp\nvoid *p;\nint a = 2, *pt;\np = (void *) &a;\npt = (int *) p;\n*pt += 3;      // a = 5\n```\n\nDấu \\* khi khai báo con trỏ nên được đặt trước và sát vào tên biến:\n\n```cpp\nint *ptr_1, *ptr_2;\n// LƯU Ý\nint* ptr1, ptr2; // không phải là 2 con trỏ mà chỉ ptr1 là con trỏ\n```\n\nLưu ý: để phân biệt con trỏ và biến thường, ta thường sử dụng prefix ptr để phân biệt con trỏ.\n\nMột con trỏ lưu giá trị là địa chỉ, vậy nên để trỏ đến một biến, ta dùng toán tử & như sau:\n\n```cpp\nint a = 2409;    // giả sử địa chỉ của biến a là 0x50\nint *ptr_a = &a; // trỏ con trỏ ptr_a đến địa chỉ của biến a, tức lúc này con trỏ ptr_a mang giá trị 0x50\ncout << *ptr_a;  // in ra giá trị lưu trữ tại địa chỉ con trỏ trỏ tới, tức là 2409\n```\n\nDo con trỏ trỏ tới biến là trỏ vào vùng nhớ mà biến đó được cấp nên khi ta thay đổi giá trị của vùng nhớ đó thì giá trị của biến cũng thay đổi theo. Quan sát ví dụ sau bạn sẽ thấy rõ hơn, biến a có giá trị là 2409, địa chỉ ô nhớ giả sử là 0x50, sau đó tạo con trỏ ptr\\_a trỏ tới biến a. Khi ta thực hiện thay đổi giá trị tại địa chỉ con trỏ đang giữ qua toán tử \\*, tức là đang thay đổi giá trị tại ô nhớ 0x50 do con trỏ đang trỏ tới a, mà 0x50 lại là địa chỉ của biến a, do đó giá trị của biến a cũng bị thay đổi theo.\n\n```cpp\nint a = 2409;    // giả sử địa chỉ ô nhớ của a là 0x50\nint *ptr_a = &a; // trỏ con trỏ ptr_a tới biến a\n*ptr_a = 2001;   // thay đổi giá trị tại ô nhớ mà con trỏ ptr_a đang giữ\ncout << a;       // 2001\n```\n\nTóm lại về con trỏ, bạn cần nhớ được:\n\nMột số bạn sẽ hơi thắc mắc dấu \\* trong lúc khai báo con trỏ và dấu \\* trước con trỏ. Dấu \\* trong lúc khai báo con trỏ chỉ là cú pháp để khai báo con trỏ mà thôi. Còn dấu \\* trước con trỏ là toán tử \\*, dùng để lấy giá trị lưu trữ tại địa chỉ mà con trỏ trỏ tới.\n\n-   \\*ptr\\_a và a đều là chỉ giá trị của a\n-   ptr\\_a và &a đều là địa chỉ của biến a\n-   Không thể thay đổi hay tự quyết định địa chỉ của biến (việc này do hệ điều hành thực hiện)\n-   Con trỏ chỉ có thể tham chiếu đến đối tượng có kiểu dữ liệu tương thích\n-   Không thể tham chiếu con trỏ đến một biểu thức hay hằng (vì biểu thức, hằng làm gì có địa chỉ)\n\n# Hằng con trỏ và đối tượng hằng\n\nNhư đã trình bảy ở trên, biến con trỏ cũng giống như một biến bình thường nhưng dùng để lưu trữ địa chỉ, con trỏ cũng có hằng con trỏ như hằng bình thường. Hằng con trỏ sẽ được khởi tạo giá trị một lần duy nhất và không được gán lại giá trị mới, hay nói cách khác là chỉ trỏ đến một đối tượng duy nhất mà thôi. Cú pháp khai báo tương tự con trỏ nhưng có từ khóa const phía trước tên biến:\n\n```cpp\nint a = 2409;\nint b = 2001;\nint *const ptr_a = &a; // con trỏ ptr_a trỏ đến biến a\nptr_a = &b; // lỗi vì ptr_a là hằng con trỏ, không thể gán giá trị khác được\n```\n\nĐối tượng hằng tức là một con trỏ mà ta không thể sử dụng toán tử \\* để gán lại giá trị tại vùng nhớ mà nó trỏ tới. Đối tượng hằng vẫn có thể trỏ đến đối tượng khác được.\n\n```cpp\nint a = 2409;\nconst int *ptr_a = &a; // con trỏ otr_a trỏ đến biến a\n*ptr_a = 2001; // lỗi vì ptr_a là đối tượng hằng, không thể gán giá trị qua toán tử *\n```\n\nBạn không muốn thay đổi giá trị và cũng không muốn tham chiếu lại vào biến khác bạn có thể kết hợp cả hai như sau:\n\n```cpp\nint a = 2409;\nconst int *const ptr_a = &a;\n```\n\n# Con trỏ NULL\n\nCon trỏ NULL (NULL pointer) hay con trỏ trỏ vào NULL là con trỏ không trỏ vào đâu cả, nó khác với con trỏ chưa được khởi tạo. Bởi vì khi được khai báo, con trỏ không được khởi tạo giá trị thì sẽ mang giá trị rác. Do đó, khi làm việc với con trỏ, khi chưa trỏ vào đâu cả thì ta nên khởi gán con trỏ đó bằng NULL (vì nếu không may, ta thực hiện truy xuất đến vùng nhớ rác không tồn tại sẽ gây ra kết quả không mong muốn).\n\n```cpp\nint *ptr_a = 0;    // ptr_a là con trỏ NULL\nint *ptr_b = NULL; // NULL là macro định nghĩa sẵn bằng 0, tức là NULL\n```\n\nNgoài ra, C++ 11 còn cung cấp một từ khóa mới là nullptr, cũng là dùng để chỉ con trỏ NULL.\n\n```cpp\nint *ptr_a = nullptr;\n```\n\nĐể kiểm tra xem một con trỏ có NULL hay không ta dùng câu lệnh if.\n\n```cpp\nif (my_ptr) // true nếu con trỏ không NULL, false nếu con trỏ NULL\n\tcout << \"NOT NULL\";\nelse\n\tcout << \"NULL ptr\";\n```\n\n# Cấp phát động\n\nQua ví dụ trên, một số bạn sẽ hỏi tại sao lại dùng con trỏ chi cho mệt vậy, cứ biến tĩnh mà dùng, sao phải dùng rồi lại thêm toán tử &, \\* cho rối. Tất cả những ví dụ ở trên chỉ để cho bạn hiểu được con trỏ mà thôi, sức mạnh thực sự của con trỏ nằm ở chỗ nó được sử dụng để quản lý biến động.\n\nĐể cấp phát vùng nhớ cho một biến động ta làm như sau:\n\n```cpp\nnew <kiểu_dữ_liệu>;\n// Ví dụ\nnew int;\nnew float;\n```\n\nNếu như cấp phát vùng nhớ thành công, toán tử new sẽ trả về một con trỏ trỏ tới địa chỉ của vùng nhớ mới. Và như đã nói ở trên, biến động không có tên do đó nó được quản lý bằng con trỏ, vậy nên khi tạo biến động ta gán luôn địa chỉ của nó cho con trỏ như sau:\n\n```cpp\nint *ptr = new int; // con trỏ ptr lúc này đang trỏ tới biến động kiểu int đã được tạo\n// Bạn cũng có thể khởi tạo giá trị ngay khi khai báo như sau\nint *ptr1 = new int(2409);\n```\n\nBây giờ bạn có thể thao tác trên biến động vừa cấp phát thông qua con trỏ như sau:\n\n```cpp\n*ptr = 2001;\n```\n\nCấp phát động là yêu cầu cấp phát một vùng nhớ, do đó sẽ có thể xảy ra trường hợp không đủ bộ nhớ để cấp phát, lúc này toán tử new sẽ trả về con trỏ NULL, bạn có thể kiểm tra như sau:\n\n```cpp\nint* myPtr = new int;\nif (myPtr != nullptr)\n\tcout << \"Memory allocated\";\nelse\n\tcout << \"Bad allocate\";\n```\n\nSau khi đã sử dụng xong, dữ liệu trong vùng nhớ của biến động nên được xóa đi và trả lại cho hệ điều hành. Việc này rất quan trọng, việc không giải phóng sau khi sử dụng sẽ khiến cho vùng nhớ đó tồn tại nhưng hệ điều hành không được sử dụng do nó đã được cấp phát cho chương trình của chúng ta, dẫn đến việc rò rỉ bộ nhớ.\n\nHiện nay, hầu hết các hệ điều hành hiện đại quản lý việc cấp phát bộ nhớ một cách triệt để, mỗi khi chương trình kết thúc bộ nhớ đã được cấp phát sẽ được thu hồi lại, tuy nhiên dữ liệu trong vùng nhớ đó không được xóa, việc này cũng dẫn đến rò rỉ bộ nhớ. Do đó, bạn vẫn nên xóa và giải phóng biến động mỗi khi kết thúc chương trình hoặc sử dụng xong.\n\nViệc xóa và giải phóng vùng nhớ của biến động được thực hiện qua toán tử delete. Cú pháp như sau:\n\n```cpp\ndelete <tên_biến_con_trỏ>;\n// ví dụ\ndelete ptr_a;\n```\n\nSau khi xóa đi, vùng nhớ đó đã được xóa dữ liệu và trả lại cho hệ điều hành quản lý, tuy nhiên, con trỏ mà đang trỏ đến vùng nhớ đó vẫn đang chứa địa chỉ đó. Việc sử dụng con trỏ này sẽ gây ra hậu quả không mong muốn do biến động nó trỏ tới không còn tồn tại, do đó khi delete biến động, ta nên gán lại con trỏ NULL.\n\n```cpp\nint *ptr = new int(1);\ndelete ptr;\nptr = nullptr;\n```\n\n# Mảng động\n\nMảng động là một topic quan trọng trong C++, việc sử dụng mảng thông thường, bạn sẽ không thể thay đổi kích thước của mảng (thêm khi cần và xóa khi không cần), mảng động sẽ giải quyết việc này. Để cấp phát một mảng động, ta sử dụng toán tử new và sau kiểu dữ liệu phải cung cấp số lượng phần tử \\[size\\]:\n\n```cpp\nnew <kiểu_dữ_liệu_của_mỗi_phần_tử>[size];\n \n// ví dụ\nnew int[100];\n```\n\nHệ điều hành sẽ cấp phát cho biến động một dãy các vùng nhớ liền kề nhau, mỗi vùng nhớ có kích thước bằng với kích thước của phần tử của mảng đó. Tương tự với mảng thông thường bạn vẫn phải cung cấp kích thước mảng và kích thước đó phải là hằng. Và cũng tương tự như biến động thông thường, bạn vẫn quản lý thông qua con trỏ như sau:\n\n```cpp\nint *myArr = new int[100];\n```\n\nĐối với mảng động, toán tử new sẽ trả về con trỏ trỏ vào ô nhớ đầu tiên của vùng nhớ được cấp phát cho mảng đó. Việc thao tác với mảng động thực hiện qua con trỏ cũng tương tự như đối với mảng thông thường như sau:\n\n```cpp\nmyArr[0] = 1;\nmyArr[1] = 2;\n```\n\nHoặc thao tác theo cách “con trỏ style” như sau:\n\n```cpp\n*(myArr + 0) = 1;\n*(myArr + 1) = 2;\n```\n\nViệc thực hiện myArr + i tức là lấy địa chỉ của con trỏ myArr (tức địa chỉ phần tử đầu tiên) rồi cộng thêm i lần kích thước của mỗi vùng nhớ. Ví dụ như kiểu byte có kích thước là 4 byte, myArr đang trỏ vào phần tử đầu tiên giả sử địa chỉ 0x50, khi gọi đến myArr là phần tử đầu tiên nghĩa là không cộng thêm gì hết là chính nó do con trỏ trỏ vào phần tử đầu tiên của mảng tức là 0x50. Gọi myArr + 1 tức là địa chỉ của phần tử đầu tiên cộng với 1 lần kích thước của kiểu int là 4 byte, tức là phần tử 0x54, sau đó dùng toán tử \\* để lấy giá trị như ví dụ bên trên.\n\nVà cũng để tránh rò rỉ bộ nhớ, không dùng đến nữa thì ta cũng phải xóa mảng động đi. Để xóa mảng động đi nó có hơi khác một chút là có dấu \\[\\] sau toán tử delete.\n\n```cpp\ndelete[] ptr;\n```\n\nVậy thì việc thay đổi kích thước mảng thực hiện ra làm sao? Bạn sẽ không có phương thức hỗ trợ nào mà phải làm thủ công. Tức là bạn sẽ phải tạo một mảng mới với kích thước phần tử mới, sau đó copy phần tử sang mảng mới và xóa mảng cũ đi như sau:\n\n```cpp\n// Mảng ban đầu\nint size = 3;\nint* arr = new int[size] {1, 2, 3};\n \n// Tạo mảng mới\nint newSize = 5;\nint* newArr = new int[newSize];\n \n// Copy phần tử từ mảng cũ sang mảng mới\nfor (auto i = 0; i < size; i++)\n\tnewArr[i] = arr[i];\n \n// Xóa mảng cũ đi\ndelete[] arr;\narr = nullptr;\n```\n\nVề bản chất, mảng thông thường thật ra chính là một mảng động được một hằng con trỏ trỏ tới:\n\n```cpp\nint *const arr = new int[10];\n```\n\nDo nó hơi phức tạp nên người ta thường sử dụng lớp vector cũng được dựa trên con trỏ và mảng động. Bạn có thể xem bài viết *Vector trong C++*[\\[1\\]](#tham-khao) của mình.\n\n# Con trỏ trỏ vào con trỏ\n\nCon trỏ cũng giống như một biến thông thường nên nó sẽ có địa chỉ, do đó, một con trỏ có thể được một con trỏ khác trỏ tới. Ví dụ;\n\n```cpp\nint *a = new int(2409);\nint **ptr_a = &a; // con trỏ ptr_a trỏ vào con trỏ a\n```\n\nKhi một con trỏ trỏ vào con trỏ, con trỏ đó sẽ giữ giá trị là địa chỉ của con trỏ mà nó trỏ tới, vậy nên nếu muốn lấy giá trị của biến của con trỏ mà nó đang trỏ tới ta phải dùng hai lần toán tử \\*, lần 1 là để lấy địa chỉ của con trỏ nó trỏ tới đang giữ, lần 2 là để lấy giá trị được lưu trữ tại địa chỉ mà biến của con trỏ mà nó trỏ tới đang giữ.\n\n```cpp\nint *a = new int(2409); // giả sử biến động có địa chỉ 0x50 và con trỏ có địa chỉ 0x70\nint **ptr_a = &a; // con trỏ ptr_a trỏ vào con trỏ a tức ptr_a mang giá trị 0x70\ncout << *ptr_a;   // được 0x50 là giá trị của con trỏ nó trỏ tới đang giữ tức địa chỉ của a đang giữ\ncout << **ptr_a;  // được 2409 do nó lấy giá trị lưu trữ tại địa chỉ a đang giữ chính là 0x50\n```\n\nCon trỏ trỏ vào con trỏ được ứng dụng để xây dựng mảng hai chiều như sau:\n\n```cpp\n// Tạo ra mảng 2 chiều 10x10\nint** arr = new int* [10];\nfor (auto i = 0; i < 10; i++)\n\tarr[i] = new int[10];\n \n// truy xuất phần tử giống như mảng bình thường\n \n// Giải phóng vùng nhớ\nfor (auto i = 0; i < 10; i++)\n\tdelete[] arr[i];\ndelete[] arr;\narr = nullptr;\n```\n\nViệc sử dụng con trỏ đối với mảng một chiều đã khá rắc rối rồi nên mình sẽ không đi sâu vào mảng nhiều chiều mà chỉ giới thiệu cho các bạn biết vậy thôi. Bạn nên sử dụng lớp vector như trong bài viết *Vector trong C++*[\\[1\\]](#tham-khao) mình đã có giới thiệu.\n\n# Con trỏ và hàm\n\nCon trỏ là một kiểu dữ liệu, do đó nó có thể được sử dụng trong lúc truyền tham số cho hàm hoặc kiểu dữ liệu trả về.\n\n```cpp\nint* doSomething(int* n)\n{\n\tcout << *n; // giá trị tại địa chỉ mà con trỏ n đang trỏ tới\n\tcout << n;  // địa chỉ con trỏ n đang lưu trữ\n\treturn n;\n}\n```\n\nĐể truyền đối số vào tham số trong hàm, ta phải truyền cùng kiểu pointer hoặc là một địa chỉ như khi bạn tham chiếu con trỏ vậy.\n\n```cpp\nint n = 5;\ndoSomething(&n);\n \n// Hoặc\nint *pt = new int(5);\ndoSomething(pt);\n```\n\nLưu ý do thao tác trên con trỏ là thao tác trên địa chỉ ô nhớ, vậy nên thay đổi giá trị tại địa chỉ ô nhớ cũng làm thay đổi luôn giá trị của biến nó tham chiếu tới tương tự như biến tham chiếu vậy.\n\n# Con trỏ hàm\n\nNgoài những loại con trỏ trên, ta còn có một loại con trỏ đặc biệt nữa đó chính là con trỏ hàm (function pointer). Để khai báo một con trỏ hàm, ta sử dụng cú pháp sau:\n\n```cpp\n<kiểu_dữ_liệu> (*<tên_con_trỏ>)([các_tham_số]);\n// Ví dụ\nint (*funcPtr_sum)(int a, int b);\n// Bạn có thể đặt tên tham số hoặc là không như thế này\nint (*funcPtr_sum)(int, int);\n```\n\nBạn có thể hiểu đơn giản rằng `<kiểu_dữ_liệu>` chính là kiểu dữ liệu mà hàm trả về, `<tên_con_trỏ>` là tên hàm, dấu ngoặc tròn ”()” bên ngoài là bắt buộc để nói cho compiler biết đó là một con trỏ hàm và các tham số truyền vào giống như tham số truyền vào hàm vậy thôi và được đặt trong dấu ngoặc tròn.\n\nĐể trỏ tới một hàm, các bạn làm như sau:\n\n```cpp\nint sum(int a, int b)\n{\n\treturn a + b;\n}\n \nfuncPtr_sum = sum;     // nên dùng\n// Hoặc có thể dùng toán tử &\nfuncPtr_sum = ∑\n// Cả hai đều tương đương như sau\n// Tại sao thì sẽ được giải thích bên dưới\n```\n\nCác kiểu dữ liệu và tham số của con trỏ hàm phải tương đương với các tham số và kiểu dữ liệu trả về của hàm mà các bạn muốn trỏ tới. Như trong ví dụ trên kiểu trả về của hàm sum là int thì kiểu dữ liệu khai báo con trỏ cũng phải là int và kiểu dữ liệu tham số của con trỏ cũng giống với tham số hàm sum. Và để gọi hàm mà con trỏ đó trỏ tới, bạn chỉ cần thực hiện như cách sau:\n\n```cpp\nfuncPtr_sum(4, 5);    // được 9, nên dùng cách này\n// Hoặc dùng theo cách \"con trỏ style\"\n(*funcPtr_sum)(3, 4); // được 7\n```\n\nCó thể nhiều bạn sẽ hỏi tại sao phải sử dụng con trỏ hàm cho mất công vậy, mình có thể gọi trực tiếp hàm đó mà. Đúng thật là vậy nhưng khi sử dụng con trỏ hàm, bạn sẽ làm được một thứ mà không cách nào thực hiện được nếu không có con trỏ hàm đó chính là truyền tham số là hàm.\n\nĐể thực hiện truyền con trỏ hàm cho hàm, ta cũng thực hiện truyền như truyền một con trỏ thông thường. Hãy xem ví dụ sau để hiểu rõ hơn:\n\n```cpp\nint sum(int a, int b)\n{\n\treturn a + b;\n}\n \nvoid myFunc(int (*func_ptr)(int, int))\n{\n\tfunc_ptr(4, 5);\n}\n \n// bên trong hàm main\nint (*funcPtr_sum)(int, int) = sum;\nmyFunc(funcPtr_sum);        // được 9\n```\n\nMột số bạn quen với một số ngôn ngữ hiện đại thì sẽ quen cách truyền hàm cho hàm, tuy nhiên có thể bạn không hiểu được bản chất tại sao nó như vậy. Qua con trỏ hàm bạn có thể hiểu được cách thức mà một hàm có thể được truyền cho một hàm.\n\nCó một só điều bạn cần lưu ý về con trỏ hàm như sau:\n\n-   Không như con trỏ thông thường, con trỏ hàm không phải là con trỏ trỏ vào vùng nhớ mà nó trỏ vào code. Có thể hiểu là nó trỏ vào điểm bắt đầu của một hàm hoặc là nó đang tham chiếu đến hàm đó và nó là nick name của hàm đó, do đó ta có thể gọi nó thay vì trực tiếp gọi hàm.\n-   Con trỏ hàm không cần cấp phát hay giải phóng vì nó không trỏ vào vùng nhớ.\n-   Bạn có thấy ở ví dụ trên mình có thể sử dụng toán tử & hoặc không, và cũng tương tự đối với việc sử dụng toán tử \\*. Đó là do tên hàm có thể được sử dụng để lấy địa chỉ (hay điểm bắt đầu) của hàm, do đó tên hàm giống như đã bao gồm toán tử & rồi.\n-   Giống như một con trỏ thông thường, ta cũng có thể có một mảng con trỏ hàm. Con trỏ hàm cũng có thể được sử dụng như để rẽ nhánh như sau:\n-   Giống như con trỏ thông thường, con trỏ hàm có thể được truyền cho hàm, giống như ví dụ bên trên. Loại con trỏ này rất thường đường sử dụng trong C++.\n\n# Con trỏ và đối tượng\n\nCon trỏ không có kiểu dữ liệu cụ thể mà phụ thuộc vào đối tượng nó trỏ vào, do đó nó có thể là bất kỳ bao gồm kiểu dữ liệu do người dùng định nghĩa như struct hay class. Ví dụ:\n\n```cpp\nstruct MyStruct\n{\n\tint count;\n};\n \nMyStruct mStruct;\nmStruct.count = 5;\n \nMyStruct *ptr = &mStruct;\n```\n\nThông thường để gọi một thuộc tính hay một phương thức của một đối tượng, ta sử dụng dấu chấm (.), nhưng đối với con trỏ khi đã trỏ đến đối tượng, ta sử dụng dấu mũi tên (→) như sau:\n\n```cpp\n// Biến thông thường\ncout << mStruct.count; // 5\n// thông qua con trỏ\ncout << ptr->count;      // 5\n```\n\nTương tự đối với class:\n\n```cpp\nclass MyClass\n{\npublic:\n\tint count;\n \n\tMyClass(int n)\n\t{\n\t\tcount = n;\n\t}\n \n\tvoid print() const\n\t{\n\t\tcout << count << endl;\n\t}\n};\n \nMyClass mClass(5);\nMyClass *ptr = &mClass;\ncout << ptr->count;  // 5\nptr->print();        // 5\n```\n\nTa cũng có thể thực hiện cấp phát động như sau:\n\n```cpp\nMyStruct *ptr_struct = new MyStruct;\nMyClass *ptr_class = new MyClass(5);\n```\n\n# Tổng kết\n\nSử dụng con trỏ là một kĩ thuật rất quan trong, bạn sẽ cần sử dụng nó thật thành thạo để có thể học tốt các môn như Lập trình hướng đối tượng, Cấu trúc dữ liệu và giải thuật…\n\nVậy là trong bài viết này, mình đã giới thiệu cho các bạn về con trỏ, bài viết này khá là dài nhưng có thể vẫn chưa đầy đủ vì con trỏ trong C++ là một kỹ thuật thật sự rất là hay mà nếu bạn giỏi C++, bạn phải nắm được hết tối thiểu những thứ mình đã giới thiệu trong bài viết. Cảm ơn các bạn đã dành thời gian theo dõi bài viết, nếu như bạn thấy hay, đừng quên chia sẻ với bạn bè. Cảm ơn các bạn rất nhiều!\n\n# Tham khảo\n\n1. [Vector trong C++](/blog/vector-trong-cpp)","tags":["Lập trình","C++"]},{"slug":"co-ban-ve-class-trong-cpp","title":"Cơ bản về Class trong C++","date":"2020-03-25","topic":"tutorial","summary":"Class hay lớp là một mô tả trừu tượng (abstract) của nhóm các đối tượng (object) có cùng bản chất, ngược lại mỗi một đối tượng là một thể hiện cụ thể (instance) cho những mô tả trừu tượng đó. Một class trong C++ sẽ có các đặc điểm sau:","body":"![CoBanVeClassTrongC++](/api/uploads/82e0af94581f61b2c3ea.jpg)\n\n# Class là gì?\n\nClass hay lớp là một mô tả trừu tượng (abstract) của nhóm các đối tượng (object) có cùng bản chất, ngược lại mỗi một đối tượng là một thể hiện cụ thể (instance) cho những mô tả trừu tượng đó. Một class trong C++ sẽ có các đặc điểm sau:\n\n-   Một class bao gồm các thành phần dữ liệu (thuộc tính hay property) và các phương thức (hàm thành phần hay method).\n-   Class thực chất là một kiểu dữ liệu do người lập trình định nghĩa.\n-   Trong C++, từ khóa class sẽ chỉ điểm bắt đầu của một class sẽ được cài đặt.\n\nVí dụ về một class đơn giản, class Car. Một chiếc xe hơi vậy thì sẽ có chung những đặc điểm là đều có vô lăng, có bánh xe nhiều hơn 3, có động cơ… Đó là một class, một cái model hay mẫu mà người ta đã quy định là nếu đúng như vậy thì nó là xe hơi. Nhưng mà xe thì có thể có nhiều hãng khác nhau, BMW, Vinfast, Toyota… Thì mỗi hãng xe lại có những model xe khác nhau nhưng chúng đều là xe hơi. Vậy thì trong lập trình cũng vậy, class là quy định ra một mẫu, một cái model mà các thể hiện của nó (instance) hay đối tượng (object) phải tuân theo.\n\n## Khai báo class và sử dụng class\n\nCú pháp khai báo một class cơ bản trong C++ như sau:\n\n```cpp\nclass <ClassName> {\n    <access_modifier>:\n        <data_type> property;\n \n        <return_type> <method_name>(arguments) {\n            return <something_match_return_type>;\n        }\n \n        <_return_type> <_method_name>(_arguments);\n};\n \n<_return_type> <ClassName>::<_method_name>(_arguments) {\n    return <something_match_return_type>;\n}\n```\n\nVí dụ một class cơ bản:\n\n```cpp\nclass Person {\n    public:\n        string firstName; // property\n        string lastName;  // property\n        int age;          // property\n \n        void fullname() { // method\n            cout << firstName << ' ' << lastName;\n        }\n};\n```\n\nTrở lại với ví dụ ở đầu bài viết, class ở đây là Car, vậy thì thuộc tính của nó chính là speed, HP,… còn phương thức chính là run, turn left, right, turn on the light…\n\n**Lưu** ý: các thuộc tính có thể bị trùng tên với các tham số trong các phương thức, vậy nên chúng ta nên dùng this→ hoặc toán tử phân giải phạm vi (::), ví dụ:\n\n```cpp\nclass Person {\n    public:\n        string firstName;\n        string lastName;\n        int age;\n \n        void fullname() {\n            cout << this->firstName << ' ' << Person::lastName;\n        }\n};\n```\n\nNói qua một chút về con trỏ this, con trỏ this đề cập đến thể hiện hay instance của class đó. Do đó, thông qua con trỏ this, ta có thể truy cập đến các thuộc tính hoặc phương thức thuộc class đó như trên ví dụ bên trên.\n\nĐối với toán tử phạm vi :: dùng để xác định phương thức hoặc thuộc tính được gọi thuộc lớp nào. Như trong ví dụ trên là truy xuất thuộc tính lastName thuộc lớp Person. Nếu như gọi từ namespace hoặc emum thì toán tử :: được dùng để gọi thành viên của namepsace hoặc enum đó. Ngoài ra, toán tử phân giải phạm vi nếu không có tên lớp phía trước thì được dùng để gọi một biến bên ngoài scope. Ví dụ:\n\n```cpp\nint x;\nint main()\n{\n    int x = 2;\n    ::x = 3; // x ở ngoài\n    return 0;\n}\n```\n\nSự khác biệt giữa this và class khá rõ ràng, this (ám chỉ thể hiện của class đang sử dụng this) chỉ sử dụng được trong class, còn đối với toán tử :: có thể sử dụng được cả ở trong và ngoài class. Quá rõ ràng, nếu dùng this ở ngoài thì biết nó chỉ thằng nào đúng không! Bạn không thể thay ::x ở ví dụ trên thành this→x hay →x được.\n\nCú pháp tạo object của một class và sử dụng các thuộc tính và phương thức:\n\n```cpp\n// tạo một object\n<className> <object>;\n// gán giá trị cho thuộc tính của object\n<object>.property = <value>;\n \n// có thể sử dụng property như một biến thông thường\ncout << <object>.property;\n// có thể sử dụng method như một hàm thông thường\n<object>.method();\n```\n\n**Lưu ý**: chỉ những thuộc tính và phương thức public thì mới có thể được sử dụng như cách trên. Ví dụ về cách sử dụng class:\n\n```cpp\nPerson person;\nperson.firstName = \"Khiem\";\nperson.lastName = \"Le\";\nperson.fullname(); // sẽ in ra màn hình là \"Khiem Le\"\n```\n\n## Access modifiers & properties declaration\n\nAccess modifier là phạm vi truy cập của các thuộc tính và phương thức sẽ được khai báo bên dưới nó. Có 3 phạm vi truy cập trong C++ là public, private và protected.\n\n-   Các thuộc tính và phương thức khai báo public thì có thể được truy cập trực tiếp thông qua instance của class đó. Các thuộc tính nên khai báo là public nếu bạn không có ràng buộc điều kiện trước khi gán (người dùng có thể thoải mái gán giá trị) hoặc bạn không cần xử lý trước khi trả về giá trị thuộc tính;\n-   Đối với private thì chỉ có thể được truy cập gián tiếp qua các phương thức public (Getter và setter). Các thuộc tính private thường được sử dụng khi bạn không mong muốn người khác có thể tùy ý gán giá trị hoặc là bạn muốn xử lý trước khi trả về giá trị.\n-   Đối với protected, các phương thức và thuộc tính chỉ có thể truy cập qua các class kế thừa nó hoặc chính nó (sẽ được nói kĩ hơn trong bài kế thừa C++).\n\nVí dụ của access modifier:\n\n```cpp\nclass MyClass\n{\n\tpublic:\n\t\tint public_property;\n \n\tprivate:\n\t\tint _private_property;\n \n\t// protected sẽ được trình bày trong bài kế thừa và đa hình trong C++\n};\n```\n\nĐối với quy cách đặt tên biến, bạn có thể sử dụng PascalCase, CammelCase… nhưng đối với các thuộc tính và phương thức private bạn nên đặt tên có dấu \\_ đầu. Ví dụ như \\_privateProp. Trong một số ngôn ngữ bật cao, thậm chí đã không còn từ khóa private mà thay vào đó sẽ chỉ là dấu \\_ trước tên biến (ví dụ như Dart).\n\n## Method declaration\n\nPhương thức cũng giống như một hàm bình thường, bạn cũng có thể không trả về giá trị, có thể có hoặc không có tham số, có thể override hàm… Đối với các tham số truyền vào phương thức, bạn cũng có thể đặt tên trùng với thuộc tính của class, sử dụng kết hợp với toán tử :: và con trỏ this. Hoặc bạn có thể đặt tên khác với thuộc tính (thường thì sẽ thêm dấu \\_ trước tên tham số như là thuộc tính private vậy).\n\nĐối với phương thức thì có hai cách định nghĩa thi hành: định nghĩa thi hành trong lúc định nghĩa class và định nghĩa thi hành bên ngoài class.\n\nĐịnh nghĩa thi hành bên trong class:\n\n```cpp\nclass Animal {\n    public:\n        string sound;\n \n        void makeNoise() {\n            cout << sound;\n        }\n};\n```\n\nĐịnh nghĩa thi hành bên ngoài class:\n\n```cpp\nclass Animal {\n    public:\n        string sound;\n \n        void makeNoise();\n};\n \nvoid Animal::makeNoise() {\n    cout << sound;\n}\n```\n\n**Lưu ý**: các phương thức không làm thay đổi giá trị thuộc tính của đối tượng thì nên có từ khóa “const” trước phần thân. Ví dụ:\n\n```cpp\nclass Animal {\n    public:\n        string sound;\n \n        void makeNoise() const;\n};\n \nvoid Animal::makeNoise() const {\n    cout << sound;\n}\n```\n\n## Getter & setter\n\nĐối với thuộc tính private, ta không thể truy cập trực tiếp từ bên ngoài, vậy có cách nào để truy cập? Đây là lúc sử dụng phương thức. Các phương thức lấy giá trị của thuộc tính được gọi là getter, các phương thức gán giá trị cho thuộc tính được gọi là setter.\n\n```cpp\nclass MyClass {\n    private:\n        int _age;\n \n    public:\n        int getAge() {         // getter\n            return _age;\n        }\n \n        void setAge(int age) { // setter\n            _age = age;\n        }\n};\n```\n\nViệc sử dụng các thuộc tính private nhằm mục đích không cho người khác tùy ý thay đổi giá trị của thuộc tính đó, ngoài ra còn giúp bạn xử lý kết quả trước khi trả về cho người yêu cầu. Việc đó được thực hiện thông qua getter và setter. Ví dụ:\n\n```cpp\nclass MyClass {\n    private:\n        int _age;\n \n    public:\n        bool isOldEnough() {\n            if (_age >= 18)\n                return 1;\n            return 0;\n        }\n \n        int getAge() {\n            return _age;\n        }\n \n        void setAge(int age) {\n            if (age < 18) {\n                cout << \"You are not old enough\\n\";\n            } else {\n                _age = age;\n            }\n        }\n};\n```\n\nLưu ý cách đặt tên getter và setter. Bạn nên đặt get vào trước tên getter và set vào trước tên setter như ví dụ bên trên của mình. Và cũng theo như phần lưu ý cuối mục “Method declaration” ở trên, các getter nên đặt là “const” bởi vì getter chỉ lấy giá trị chứ không thay đổi giá trị thuộc tính.\n\n## Constructor\n\nConstructor hay hàm dựng là một hàm đặc biệt, nó sẽ được gọi ngay khi chúng ta khởi tạo một object. Vậy thì tại sao chúng ta lại cần có constructor?\n\nNếu bạn để ý ví dụ trên bạn sẽ thấy ta phải khởi tạo một object sau đó gán các property và sử dụng, việc này rất tốn thời gian. Constructor sẽ giúp chúng ta giải quyết việc này. Cú pháp khai báo một constructor giống với hàm nhưng không có kiểu dữ liệu trả về:\n\n```cpp\nclass MyClass {\n  public:\n    MyClass() { // constructor\n      cout << \"Hello World!\";\n    }\n};\n \n// sử dụng\nMyClass object; // sẽ in ra màn hình \"Hello World\"\n```\n\nLưu ý constructor phải được khai báo public. Công dụng chính của constructor chính là khởi gán các thuộc tính, vậy nên constructor thường được định nghĩa như sau:\n\n```cpp\nclass Person {\n\tpublic:\n\t    string firstName;\n\t    string lastName;\n\t    int age;\n \n\t    Person(string _firstName, string _lastName, int _age)\n\t    {\n\t        firstName = _firstName;\n\t        lastName = _lastName;\n\t        age = _age;\n\t    }\n \n\t    void fullname() {\n\t\t\tcout << firstName << ' ' << lastName;\n\t    }\n};\n```\n\nConstructor cũng có thể định nghĩa thi hành bên ngoài class giống như phương thức vậy:\n\n```cpp\nclass Person {\n\tpublic:\n\t    string firstName;\n\t    string lastName;\n\t    int age;\n \n\t    Person(string _firstName, string _lastName, int _age);\n \n\t    void fullname() {\n\t        cout << firstName << ' ' << lastName;\n\t    }\n};\n \nPerson::Person(string _firstName, string _lastName, int _age)\n{\n\tfirstName = _firstName;\n\tlastName = _lastName;\n\tage = _age;\n}\n```\n\nĐể khởi tạo một object thông qua constructor, ta làm như sau:\n\n```cpp\nPerson person(\"Khiem\", \"Le\", 20);\nperson.fullname(); // Khiem Le\n```\n\nNhư vậy chúng ta không cần phải set từng thuộc tính cho object đó mà khởi tạo trực tiếp qua constructor.\n\n## Destructor\n\nĐối với một số ngôn ngữ lập trình khác có thể destructor không phổ biến, nhưng đối với C++, việc được quản lý bộ nhớ một cách hoàn toàn do người lập trình làm chủ thì destructor là vô cùng cần thiết. Hãy thử nghĩ xem, trong số thuộc tính của class bạn định nghĩa có một con trỏ, mảng động… và bạn không sử dụng desctructor thì sẽ như thế nào? Đương nhiên sẽ xảy ra chuyện rò rỉ bộ nhớ và điều này cực kì không tốt. Với destructor bạn có thể xóa con trỏ đi khi object được thu hồi hoặc bạn có thể gọi tường minh destructor.\n\nCách khai báo destructor cũng giống như đối với constructor nhưng có kí hiệu ~ phía trước:\n\n```cpp\nclass MyClass {\n    public:\n        MyClass() { // constructor\n            cout << \"Constructor is executed\\n\";\n        }\n \n        ~MyClass() { // destructor\n            cout << \"Constructor is executed\\n\";\n        }\n};\n \n// Khởi tạo object\nClassName t; // gọi constructor không tường minh\n// Gọi destructor tường minh\nt.~MyClass();\n```\n\n## Static member\n\nStatic member hay thành viên tĩnh trong class C++ cũng tương tự như với static variable (biến tĩnh) trong function. Đối với function, sau khi thực hiện xong khối lệnh và thoát thì biến tĩnh vẫn sẽ không mất đi. Đối với class, thành viên tĩnh sẽ là thuộc tính dùng chung cho tất cả các đối tượng của class đó, cho dù là không có đối tượng nào tồn tại. Tức là bạn có thể khai báo nhiều object, mỗi object các thuộc tính của nó đều khác nhau nhưng riêng static thì chỉ có một và static member tồn tại trong suốt chương trình cho dù có hay không có object nào của nó hay nói ngắn gọn là dùng chung một biến static.\n\nCác thành viên tĩnh của class có thể được truy cập từ bất kì đối tượng nào của class đó hoặc thông qua toán tử phạm vi (::). Biến tĩnh cũng có phạm vi truy cập như một biến thông thường (public, private và protected). Để khai báo một biến tĩnh, ta thực hiện thêm từ khóa “static” vào trước kiểu dữ liệu, sau đó khởi tạo giá trị bên ngoài như sau:\n\n```cpp\nclass MyClass {\n    public:\n        static int count;\n};\n \nint MyClass::count = 0;\n```\n\nBây giờ bạn có thể sử dụng biến tĩnh như cách đã trình bày bên trên:\n\n```cpp\ncout << MyClass::count; // 0\nMyClass::count++; // 1\n```\n\nĐể thấy được sự “dùng chung” của static member, các bạn có thể xem ví dụ sau:\n\n```cpp\nMyClass khiemle;\nkhiemle.count = 100;\ncout << MyClass::count; // 100\n```\n\nTrong ví dụ trên rõ ràng là mình chỉ set thuộc tính của object khiemle thôi nhưng mà khi in biến count qua toán tử phạm vi thì vẫn được kết quả là 100. Nghĩa là tất cả các object đều dùng chung thuộc tính static đó. Bạn có thể đặt thuộc tính tĩnh là hằng như sau:\n\n```cpp\nclass MyClass {\n    public:\n        static const int count;\n};\n \nconst int MyClass::count = 0;\n```\n\nThuộc tính là const sẽ không được thay đổi trong suốt chương trình, do đó bạn có thể gán giá trị ngay khi khai báo như sau:\n\n```cpp\nclass MyClass {\n    public:\n        static const int count = 0;\n};\n \n//const int MyClass::count = 0; Không cần dòng này nữa\n```\n\nLưu ý là đối với static member không phải là hằng bạn sẽ không gán giá trị như cách trên được:\n\n```cpp\nclass MyClass {\n    public:\n        static int count = 0; // không được phép\n};\n```\n\nTiếp theo là phương thức tĩnh. Phương thức tĩnh cũng giống như thuộc tính tĩnh, chúng ta có thể gọi trực tiếp qua toán tử phạm vi mà không cần một object nào của nó tồn tại cả.\n\n```cpp\nclass MyClass {\n\tpublic:\n\t    static void sayHello() {\n\t        cout << \"Hello\";\n\t    }\n};\n \nMyClass::sayHello(); // Hello\n```\n\nLưu ý là các phương thức tĩnh sẽ chỉ truy cập được đến các biến tĩnh và phương thức tĩnh khác chứ không được truy cập thành viên khác ngoài static member.\n\n```cpp\nclass Person {\n\tpublic:\n\t    string firstName;\n\t    string lastName;\n \n\t    static void fullname() {\n\t        cout << firstName << ' ' << lastName; // không hợp lệ\n\t    }\n};\n```\n\n# Tổng kết\n\nQua bài viết này, mình đã giới thiệu cho các bạn về class, thuộc tính, phương thức và hàm dựng trong C++. Trong bài viết thì cách đặt tên hàm, biến có phần không thống nhất với nhau, bạn nên chọn một cách đặt tên phù hợp nhất với bản thân để clean code. Nếu có sai xót hoặc thắc mắc gì, các bạn có thể để lại bình luận bên dưới bài viết để giúp mình phát triển bài viết tốt hơn. Cảm ơn các bạn đã theo dõi bài viết!","tags":["Lập trình","C++"]},{"slug":"vector-trong-cpp","title":"Vector trong C++","date":"2020-03-24","topic":"tutorial","summary":"Bạn đã bao giờ phát chán việc quản lý mảng động qua con trỏ trong C++ chưa? Hay việc mỗi khi bạn muốn resize kích thước mảng động trong C++, bạn phải tạo mảng mới, copy các phần tử qua mảng mới, rồi lại xóa mảng cũ đi. Quá phiền phức, tốn quá nhiều…","body":"![Vector trong C++](/api/uploads/644c59831945c9d1402a.jpg)\n\n# Vector là gì và tại sao nên dùng vector?\n\nBạn đã bao giờ phát chán việc quản lý mảng động qua con trỏ trong C++ chưa? Hay việc mỗi khi bạn muốn resize kích thước mảng động trong C++, bạn phải tạo mảng mới, copy các phần tử qua mảng mới, rồi lại xóa mảng cũ đi. Quá phiền phức, tốn quá nhiều thời gian, vì thế mà C++ còn cung cấp cho chúng ta một kiểu nữa đó chính là kiểu vector. Vậy thì vector là gì?\n\nVector là một chuỗi các phần tử có cùng kiểu dữ liệu, cũng giống như mảng bình thường trong C++. Vậy thì tại sao phải dùng nó?\n\nĐầu tiên, vector có thể tự tăng kích thước của nó mỗi khi ta thực hiện thêm một phần tử vào vector. Thứ 2, vector có thể tự giải phóng bộ nhớ khi ta thực hiện xong đoạn code và thoát ra khỏi scope chứa vector đó, việc này nhầm tránh rò rỉ bộ nhớ khi ta quên delete\\[\\] như con trỏ. Thứ ba là vector cung cấp các hàm cần thiết để chúng ta cho thể thao tác với mảng một cách dễ dàng.\n\n# Cú pháp khai báo và các hàm cơ bản\n\nVới quá nhiều lợi ích của vector thì việc gì chúng ta phải xử dụng mảng động để quản lý cho mất công đúng không nào! Hãy cùng tìm hiểu về cách sử dụng vector ngay sau đây.\n\n## Khai báo vector\n\nĐể sử dụng vector bạn cần thêm thư viện vector trước. Cú pháp khai báo một vector như sau:\n\n```cpp\n#include <vector>\n//...\nvector<[kiểu_dữ_liệu]> tên_vector;\n```\n\nVậy là chúng ta đã có một vector với mỗi phần tử có kiểu dữ liệu là \\[kiểu\\_dữ\\_liệu\\]. Bạn có thể gán giá trị trực tiếp cho vector ngay khi khởi tạo như ví dụ sau:\n\n```cpp\nvector<int> arr = {1, 2, 3};\n// tạo một vector có ba phần tử là 1, 2, 3\n```\n\nBạn cũng có thể khởi tạo một vector và gán giá trị một vector khác cho nó như sau:\n\n```cpp\nvector<int> A = {1, 2, 3};\nvector<int> B = A;\n// B cũng sẽ mang giá trị của A là {1, 2, 3}\n```\n\n## Truy xuất phần tử của vector\n\nTruy xuất phần tử trong vector có thể được thực hiện qua hai cách là qua phương thức at() hoặc qua dấu \\[\\] như mảng thông thường.\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.at(2); // được 3 vì vị trí số 2 chính là 3\n// Hoặc\ncout << arr[2]; // ta cũng được tương tự như trên\n// gán phần tử\narr.at(0) = 0; // gán lại phần tử 0 bằng 0\n// hoặc\narr[1] = 9; // gán lại phần tử 1 bằng 7\n```\n\nCả hai cách đều có công dụng như nhau vậy thì sự khác nhau giữa chúng là gì? Chính là khả năng kiểm tra phạm vi. Đối với phương thức at(), bạn có thể dùng try catch để bắt lỗi như sau:\n\n```cpp\nvector<int> arr = {1, 2, 3};\ntry\n{\n\tcout << arr.at(-1);\n}\ncatch (const std::out_of_range& oor)\n{\n\tstd::cerr << \"Out of Range error: \" << oor.what() << '\\n';\n}\n```\n\n## Lấy phần tử đầu và cuối của vector\n\nĐể lấy phần tử đầu và cuối ta sử phương thức hàm font() và back().\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.front(); // được phần tử đầu là 1\ncout << arr.back(); // được phần tử cuối là 3\n```\n\n## Thêm phần tử vào cuối vector\n\nĐể thêm phần tử vào cuối vector ta sử dụng phương thức push\\_back().\n\n```cpp\nvector<int> arr = {1, 2, 3};\narr.push_back(4);\n// Mảng sau khi thêm là {1, 2, 3, 4}\n```\n\nLưu ý sau khi thêm vào thì size của vector sẽ tăng lên 1 và nếu size vượt quá capacity thì capacity cũng sẽ tự động được tăng lên. (size và capacity sẽ được trình bày bên dưới).\n\n## Loại bỏ phần tử cuối vector\n\nĐể loại bỏ đi phần tử cuối cùng của vector, ta sử dụng phương thức pop\\_back().\n\n```cpp\nvector<int> arr = {1, 2, 3};\narr.pop_back(4);\n// Mảng sau khi loại bỏ phần tử cuối là {1, 2, 3}\n```\n\nLưu ý sau khi loại bỏ phần tử cuối thì size sẽ tự giảm xuống 1 nhưng capacity không thay đổi (size và capacity sẽ được trình bày bên dưới).\n\n## Số phần tử của vector\n\nĐể biết được số phần tử của vector ta sử dụng phương thức size().\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.size(); // ta sẽ được 3 vì vector có 3 phần tử\n```\n\n## Kích thước của vector\n\nĐể biết được kích thước của vector ta dùng phương thức capacity().\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.capacity(); //  ta sẽ được 3 vì vector có kích thước là 3 phần tử\n```\n\nCó nhiều bạn bị nhầm giữa số lượng phần tử (size) và kích thước vector (capacity). Kích thước là số lượng phần tử nhiều nhất mà vector đó từng chứa còn số lượng phần tử là số lượng phần tử mà vector đó hiện tại đang chứa. Các bạn có thể xem ví dụ sau để hiểu thêm:\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.size(); // được 3 vì vector đang chứa 3 phần tử\ncout << arr.capacity(); // được 3 vì vector chứa nhiều nhất là 3 phần tử\narr.pop_back(); // lấy đi phần tử cuối\ncout << arr.size(); // được 2 vì vector đang chứa 2 phần tử\ncout << arr.capacity(); // vẫn được 3 vì vector này từng chứa nhiều nhất 3 phần tử\n```\n\n## Thay đổi số phần tử của vector\n\nBạn có thể dùng phương thức resize() để thay đổi số phần tử của vector.\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.size(); // được 3 vì vector đang giữ 3 phần tử\narr.resize(1);\ncout << arr.size(); // được 1 vì vector đã được resize về 1\n```\n\nLưu ý là sau khi resize thì vector sẽ chỉ lấy đủ số phần tử đã set còn tất cả những phần tử sau sẽ bỏ đi.\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.size(); // được 3 vì vector đang giữ 3 phần tử\narr.resize(1);\ncout << arr.size(); // được 1 vì vector đã được resize về 1\n \n// Xét ví dụ sau\ncout << arr.at(2);\ncout << arr[2];\n// cả hai đều lỗi vì truy cập phần tử lớn hơn size của vector\n```\n\n## Xóa hết phần tử của vector\n\nĐể xóa tất cả các phần tử của vector ta dùng phương thức clear(). Sau khi xóa thì size cũng sẽ trở về 0.\n\n```cpp\nvector<int> arr = {1, 2, 3};\ncout << arr.size(); // được 3\narr.clear();\ncout << arr.size(); // được 0 vì đã xoá hết phần tử của vector\n```\n\n# Lời kết\n\nTrên đây là bài viết vector trong C++ của mình, nếu có sai xót dì các bạn có thể comment bên dưới video để giúp mình sửa sai nha. Cảm ơn các bạn đã theo dõi bài viết!","tags":["Lập trình","C++"]}]}