문제 정의
복권(예: 한국 로또 6/45 또는 더블록스)은 일반적으로 일정 범위 내에서 중복 없는 숫자 조합을 요구합니다. 본 프로그램은 사용자가 직접 선정한 후보 번호 그룹(예: 3개 그룹)에서 각 그룹별로 지정된 개수만큼 숫자를 무작위로 뽑아 하나의 복권 번호 세트를 완성합니다. 생성된 5세트의 번호는 콘솔에 출력됨과 동시에 CSV(Comma-Separated Values) 파일로 저장되어 엑셀과 같은 스프레드시트 프로그램에서 추가 분석이 가능하도록 합니다.
설계 접근법
이 프로그램의 핵심 기능은 두 가지입니다: 고품질의 난수 생성과 사용자 친화적인 상호작용.
- 고급 난수 생성: C++11 표준 라이브러리의
<random>헤더를 사용하여 결정론적 의사 난수를 생성합니다. 단순히rand()함수를 사용하는 것보다 더 강력하고 예측 불가능한 시퀀스를 생성하기 위해std::mt19937_64(64비트 Mersenne Twister 엔진)을 채택했습니다. 또한, 시간, 프로세스 ID, 스레드 ID 및std::random_device를 결합하여 초기 시드(seed)의 엔트로피(무작위성)를 극대화했습니다. - 모듈식 설계: 각 기능(난수 생성, 복권 번호 조합, 사용자 입력, 파일 저장)은 독립적인 함수로 분리하여 코드의 가독성과 재사용성을 높였습니다.
- 사용자 상호작용: 사용자는 세 개의 후보 번호 그룹을 각각 입력할 수 있습니다. 프로그램은 각 그룹에서 뽑을 숫자의 개수를 입력받아 합계가 총 볼 개수(예: 6개)가 되도록 검증합니다. 이후 Enter 키를 누를 때마다 5세트의 새로운 번호를 생성하며, ESC 키를 누르면 프로그램이 종료됩니다.
1. 핵심 난수 생성 함수: getRandomSubset
이 함수는 주어진 정수 벡터에서 중복 없이 count개의 요소를 무작위로 선택합니다. Fisher-Yates 셔플 알고리즘의 변형을 사용하여 리스트를 무작위로 섞은 후, 앞에서부터 필요한 개수만큼 가져오는 방식을 사용합니다.
#include <vector>
#include <algorithm> // for std::shuffle
#include <random>
#include <chrono>
#include <thread>
#include <windows.h>
// ... (다른 include)
std::vector<int> getRandomSubset(const std::vector<int>& source, size_t count) {
if (count > source.size()) {
throw std::invalid_argument("요청 개수가 소스 벡터 크기보다 큽니다.");
}
// 1. 작업 복사본 생성
std::vector<int> temp = source;
// 2. 엔트로피가 높은 시드 생성
auto now = std::chrono::high_resolution_clock::now();
auto nanosec = std::chrono::time_point_cast<std::chrono::nanoseconds>(now).time_since_epoch().count();
DWORD processId = GetCurrentProcessId();
std::thread::id threadId = std::this_thread::get_id();
std::random_device hardwareRandomness;
std::seed_seq seed_seq{
static_cast<unsigned int>(nanosec),
static_cast<unsigned int>(processId),
static_cast<unsigned int>(std::hash<std::thread::id>()(threadId)),
hardwareRandomness()
};
// 3. 64비트 Mersenne Twister 엔진 초기화
std::mt19937_64 generator(seed_seq);
// 4. 엔진 워밍업 (엔진 상태를 더욱 무작위화)
for (int warmup = 0; warmup < 100; ++warmup) {
generator();
}
// 5. Fisher-Yates 스타일 셔플 수행
for (size_t i = temp.size() - 1; i > 0; --i) {
std::uniform_int_distribution<size_t> distribution(0, i);
size_t j = distribution(generator);
std::swap(temp[i], temp[j]);
}
// 6. 셔플된 리스트의 처음 'count'개 요소 추출
std::vector<int> result(temp.begin(), temp.begin() + count);
std::sort(result.begin(), result.end()); // 결과를 오름차순으로 정렬
return result;
}
2. 복권 번호 조합 함수: composeLotteryTicket
이 함수는 세 개의 후보 그룹과 각 그룹에서 뽑을 개수를 입력받아 하나의 티켓(빨간색 공)을 완성합니다.
std::vector<int> composeLotteryTicket(const std::vector<int>& groupA,
const std::vector<int>& groupB,
const std::vector<int>& groupC,
size_t countA, size_t countB, size_t countC) {
std::vector<int> ticket;
// 각 그룹에서 난수 부분 집합 추출
std::vector<int> subsetA = getRandomSubset(groupA, countA);
std::vector<int> subsetB = getRandomSubset(groupB, countB);
std::vector<int> subsetC = getRandomSubset(groupC, countC);
// 모든 번호를 하나의 티켓으로 병합
ticket.insert(ticket.end(), subsetA.begin(), subsetA.end());
ticket.insert(ticket.end(), subsetB.begin(), subsetB.end());
ticket.insert(ticket.end(), subsetC.begin(), subsetC.end());
// 최종 오름차순 정렬 (복권 번호 표시 형식)
std::sort(ticket.begin(), ticket.end());
return ticket;
}
3. 보너스 볼 생성 함수: generateBonusBall
별도의 난수 엔진을 사용하여 1에서 16 사이의 값을 생성합니다.
int generateBonusBall() {
auto now = std::chrono::high_resolution_clock::now();
auto nanosec = std::chrono::time_point_cast<std::chrono::nanoseconds>(now).time_since_epoch().count();
DWORD processId = GetCurrentProcessId();
std::seed_seq seed{ static_cast<unsigned int>(nanosec), static_cast<unsigned int>(processId) };
std::mt19937_64 gen(seed);
for (int i = 0; i < 50; ++i) {
gen();
}
std::uniform_int_distribution<int> distribution(1, 16);
return distribution(gen);
}
4. 사용자 입력 인터페이스
사용자는 콘솔을 통해 숫자를 입력합니다. inputNumberGroup 함수는 프롬프트를 출력하고, 유효성 검사(1-33 범위, 최대 33개)를 수행하며 입력을 벡터로 저장합니다.
std::vector<int> inputNumberGroup(const std::string& prompt) {
std::vector<int> group;
int inputNum;
std::cout << prompt << std::endl;
while (std::cin >> inputNum) {
if (group.size() >= 33) {
std::cout << "최대 33개의 숫자만 입력 가능합니다." << std::endl;
break;
}
if (inputNum < 1 || inputNum > 33) {
std::cout << "1에서 33 사이의 숫자만 입력해주세요." << std::endl;
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
continue;
}
group.push_back(inputNum);
if (std::cin.peek() == '\n') break;
}
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return group;
}
5. 출력 및 저장 함수
displayAndSaveTickets 함수는 5세트의 티켓을 생성하고, 표준 출력(콘솔)과 파일 스트림에 동시에 CSV 형식으로 기록합니다.
void displayAndSaveTickets(std::ofstream& outFile,
const std::vector<int>& gA, const std::vector<int>& gB, const std::vector<int>& gC,
size_t nA, size_t nB, size_t nC) {
for (int t = 0; t < 5; ++t) {
std::vector<int> reds = composeLotteryTicket(gA, gB, gC, nA, nB, nC);
int bonus = generateBonusBall();
// 콘솔 출력
for (size_t i = 0; i < reds.size(); ++i) {
std::cout << std::setfill('0') << std::setw(2) << reds[i];
if (i < reds.size() - 1) std::cout << " ";
}
std::cout << " | " << std::setfill('0') << std::setw(2) << bonus << std::endl;
// CSV 파일 기록
for (size_t i = 0; i < reds.size(); ++i) {
outFile << std::setfill('0') << std::setw(2) << reds[i];
if (i < reds.size() - 1) outFile << ",";
}
outFile << "," << std::setfill('0') << std::setw(2) << bonus << std::endl;
}
}
6. 메인 함수 흐름 제어
메인 함수는 프로그램의 전체적인 흐름을 관리합니다.
int main() {
// 1. 사용자로부터 숫자 그룹 입력받기
std::vector<int> group1 = inputNumberGroup("첫 번째 번호 그룹을 입력하세요 (1~33):");
std::vector<int> group2 = inputNumberGroup("두 번째 번호 그룹을 입력하세요 (1~33):");
std::vector<int> group3 = inputNumberGroup("세 번째 번호 그룹을 입력하세요 (1~33):");
// 2. 총 그룹 크기 검증 (6 ~ 33 사이)
while ( (group1.size() + group2.size() + group3.size() < 6) ||
(group1.size() + group2.size() + group3.size() > 33) ) {
std::cout << "세 그룹의 총 숫자 개수는 6에서 33 사이여야 합니다. 다시 입력하세요.\n";
group1 = inputNumberGroup("첫 번째 그룹:");
group2 = inputNumberGroup("두 번째 그룹:");
group3 = inputNumberGroup("세 번째 그룹:");
}
// 3. 각 그룹에서 뽑을 개수 입력 및 검증
size_t pickA, pickB, pickC;
do {
std::cout << "각 그룹에서 뽑을 개수를 입력하세요 (합계 = 6): ";
std::cin >> pickA >> pickB >> pickC;
} while (pickA + pickB + pickC != 6);
// 4. CSV 파일 열기 및 헤더 쓰기
std::ofstream file("lottery_output.csv");
file << "R1,R2,R3,R4,R5,R6,Bonus\n";
// 5. 키보드 입력 루프 (Enter: 생성, ESC: 종료)
std::cout << "Enter 키를 누르면 5세트가 생성됩니다. ESC 키를 누르면 종료됩니다.\n";
while (true) {
int key = _getch();
if (key == 27) { // ESC
break;
} else if (key == '\r' || key == '\n') { // Enter
displayAndSaveTickets(file, group1, group2, group3, pickA, pickB, pickC);
}
}
file.close();
return 0;
}
전체 소스 코드
다음은 위에서 설명한 모든 구성 요소를 포함하는 완전한 프로그램입니다. Visual Studio 2019 환경에서 컴파일 및 실행을 확인했습니다.
// ... (모든 include와 함수 정의를 순서대로 포함)
// (위에 정의된 getRandomSubset, composeLotteryTicket,
// generateBonusBall, inputNumberGroup, displayAndSaveTickets, main)