생성

아래 명령어로 id를 생성한다. (useradd는 low level utility로 home directory 자동 생성이 안될 수 있다.근데 CentOS에서는 둘이 동일하다.)

adduser 유저id

passwd 유저id

변경

특정 그룹에 넣기(adduser로 그냥 생성하면 id와 같은 이름의 group이 생성되면서 거기 들어갔던 것 같다)

adduser -G 그룹이름 유저id


sudo권한 부여

/etc/sudoers 열어서 root 밑에 유저id 추가


삭제

userdel -r 유저id (-r옵션을 주어야 홈디렉토리까지 삭제됨에 주의, deluser라는 것도 있지만 redhat계열은 없다고 한다. 따라서 CentOS에도 없음)

groupdel 그룹이름(user생성시 같은 이름의 그룹이름이 생성되었을 수 있기 때문에 해주는 것)


조회

특정 유저id가 속한 그룹들 조회(여러 그룹에 들어가 있을 수 있다.)

groups 유저id



반응형

'Programming > Linux' 카테고리의 다른 글

리눅스 비밀번호 틀린 횟수 초과 대응방법  (0) 2019.10.07
crontab  (0) 2019.09.27
CentOS 7 방화벽  (0) 2017.12.04
리눅스 port 확인  (0) 2017.12.04
sudo 관련  (0) 2017.11.07

여기 참조했습니다.

 

기본개념

 

git branch를 아무런 옵션 없이 실행하면 (local) branch 목록을 보여준다.

1
2
3
4
$ git branch
  iss53
* master
  testing
cs

git branch -r 하면 원격 branch 목록을 보여준다.

 

git branch -d my_branch 하면 로컬에서 해당 branch를 삭제한다.

git push origin :my_branch 하면 원격에서도 삭제한다.

근데 이렇게 해도 git branch -r에서 계속 보인다면 git fetch --all --prune 하면 갱신된다.

 

아래 그림 좋아서 가져옴

단, 아래 그림에서는 Index(=stage)에서 workspace로 돌리는 명령어가 checkout으로 되어 있는데,

git 2.23부터는 "git restore --staged <file>" 이런식으로 restore를 써야함

위 그림에서 index = stage

git pull과 git fetch차이

fetch는 원격 저장소의 변경 사항을 가져오지만 로컬 저장소와의 병합은 수동으로 수행해야 하며,
pull은 변경 사항을 가져오고 자동으로 병합한다.
git pull = git fetch + git merge

 

읽기쓰기 저장소가 다른 경우가 있을수 있다.

git branch -v 해보면 fetch와 push저장소가 나오는데, 보안등의 이유로 각각 다른 설정을 사용할수도 있다.(실제 그런 경우를 본적은 없다)

$ git remote -v
origin  git@github.com:sevity/online_judge.git (fetch)
origin  git@github.com:sevity/online_judge.git (push)

 

git branch 전략

 

반응형

'Programming > Git' 카테고리의 다른 글

git 초기설정  (0) 2023.06.06
git log  (0) 2019.12.09
git 자주 쓰는 명령어 모음  (0) 2019.09.27
github  (0) 2018.11.07
--no-ff 옵션  (0) 2017.11.13

판정하고자 하는 n의 범위가 클 때는 아래 is_prime() 함수를 사용

bool is_prime(long long n)
{
      if (n<2) return false;

       for(long long it=2;it*it<=n;it++)
             if(n%it==0)  return false;

       return true;
}

좀 더 빠른 함수를 원하면 아래 버전도 있다. 그러나 둘다 시간복잡도는 O(sqrt(N))으로 동일하다. 경험적으로는 3배정도 상수배로 아래 함수가 위 함수보다 빠르다.

bool is_prime(ll n)
{
    // 코너케이스 검사
    if (n <= 1)
        return false;
    if (n <= 3)
        return true;

    // 이 부분은 아래 루프에서 가운데 5개 숫자를 건너뛸 수 있도록 하는 검사입니다
    if (n % 2 == 0 || n % 3 == 0)
        return false;

    // 소수의 개념을 사용합니다.
    // 소수는 (6*n + 1) 또는 (6*n - 1)의 형태로 표현될 수 있으므로
    // 6의 모든 배수에 대해 검사해야 하며,
    // 소수는 항상 6의 배수보다 1 더 크거나 1 더 작습니다.
  
    /* 
       1. 여기서 i는 5 + 6K의 형태이며, 여기서 K는 0 이상입니다.
       2. i+1, i+3, i+5는 짝수 (6 + 6K)입니다. N은 짝수가 아닙니다.
       3. N%2와 N%3 검사가 이전 단계에서 완료되었기 때문입니다.
       4. 따라서 i+1, i+3, i+5는 N의 약수가 될 수 없습니다.
       5. i+4는 9 + 6K 형태로, 3의 배수입니다. 
       6. N은 3의 배수가 아니므로, i+4는 N의 약수가 될 수 없습니다.
       따라서 N이 i나 i+2의 약수인지만 확인합니다.
    */
    for (ll i = 5; i * i <= n; i = i + 6)
        if (n % i == 0 || n % (i + 2) == 0)
            return false;

    return true;
}

아래 밀러-라빈 소수판정법을 쓰면 n의 범위가 클 때, 위의 최적화된 코드 보다도 대략 20배정도 빠른 성능을 얻을 수 있고, 확률적 방법이긴 하지만, 실용적인 범위내에서는 다 맞는 결과를 도출한다. 시간복잡도는 O(k * log(n)^3)정도 된다.(k=3)

n<4,759,123,141일 경우 아래 코드는 정확히 동작하며 더 큰 범위에 대해서는 추가 처리가 필요하다(unsigned int를 long long등으로 바꿔주고 2,7,61 부분을 늘려줘야 한다, 참고로 unsigned int의 범위는 0부터 4,294,967,295까지로 위 범위안에 들어간다.)

bool is_prime(unsigned int n) {
	if (n < 2) return 0;
	if (n % 2 == 0) {
		if (n == 2) return 1;
		else return 0;
	}
	int s = __builtin_ctz(n - 1);
	for (unsigned long long a: {2, 7, 61}) {
		if (a >= n) continue;
		int d = (n - 1) >> s;
		unsigned long long now = 1;
		while (d != 0) {
			if (d & 1) now = (now * a) % n;
			a = (a * a) % n;
			d >>= 1;
		}
		if (now == 1) goto success;
		for (int i = 0; i < s; ++i) {
			if (now == n - 1) goto success;
			now = (now * now) % n;
		}
		return 0;
		success:;
	}
	return 1;
}

 

에라토스테네스의 채(sieve)

1
2
3
4
5
6
7
8
9
10
11
int np[10001];  // not prime
int main(void)
{
    np[1]=1;
    for(int i=2;i*i<10001;i++)
        for(int j=2;i*j<10001;j++)
            np[i*j]=1;
 
    return 0;
}
 
cs

stl버전

vector<bool> sieve(int max)
{
    vector<bool> p(max+1, true);
    p[0] = p[1]=false;
    for(int i=2;i*i<=max;i++)
        for(int j=2;i*j<=max;j++)
            p[i*j]=false;
    return p;
}

 

 

소인수 분해

1
2
3
4
5
6
7
8
9
10
int main(){
    int t;cin >> t;
    for(int i=2; i*i<=t; i++){
        while(t%i == 0){
            printf("%d\n",i);
            t /= i;
        }
    }
    if(t > 1printf("%d",t); // 이 부분 주의
}
cs

t%i 부분에서 소수가 아니어서 문제가 될 것 같지만, 합성수의 경우 소수로 먼저 나눠떨어진 이후에 접근되어 괜찮음

 

소수 배열 생성기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void gen_primes(vector<int>&primes, int max) {
    // 아래 줄이 없으면 max가 1이하로 들어올때 빈 엘리먼트가 리턴돼서 문제되는 경우가 있음
    // 물론 max가 1 이하일때 빈 엘리먼트가 리턴되는게 더 정확한 동작이긴 한데
    // 라이브러리의 관용성 측면에서 하나라도 엘리먼트가 있는게 함수밖 예외처리가 간단해짐
    primes.push_back(2);  
    for (int i = 3; i <= max; i++)
    {
        bool prime = true;
        for (int j = 0; j < primes.size() && primes[j] * primes[j] <= i; j++)
        {
            if (i % primes[j] == 0)
            {
                prime = false;
                break;
            }
        }
        if (prime) primes.push_back(i);
    }
}
cs

소수로 이루어진 배열이 필요하면 위의 함수를 쓰면 된다. 예를 들어 이 문제의 답안에 쓰일 수 있다.

근데, 신기하게도, 아래처럼 먼저 체로 거르고 벡터에 담는게 두배정도 빠르다.

vector bool로 바꾸면서 또 두배 빨라짐

1
2
3
4
5
6
7
8
9
10
11
12
13
void gen_primes(vector<int>&primes, int max) {
    vector<bool> np(max + 1false);
    for (int i = 2; i <= max; i++) {
        if (np[i]) continue;
        for (int j = 2 * i; j <= max; j += i) {
            np[j] = true
        }
    }
    primes.push_back(2);
    for (int i = 3; i <= max; i++) {
        if (!np[i]) primes.push_back(i);
    }
}
cs

아래처럼 바꾸니까, 또 위에서 3배 빨라졌다 (총 12배 ㄷ)

1
2
3
4
5
6
7
8
9
void gen_primes(vector<int>& primes, int max) {
    vector<bool> np(max + 1false);
    for (int i = 3; i * i <= max; i += 2)
        if (!np[i])
            for (int j = i * i; j <= max; j += i + i)
                np[j] = true;
    primes.push_back(2);
    for (int i = 3; i <= max; i += 2if (!np[i]) primes.push_back(i);
}
cs

 

 

반응형

문제는 여기 참조

 

브루트포스 $O(N)$

1
2
3
4
5
6
for (int i = 1; i <= n; i++) {
    if (n%i == 0) {
        printf("%d ", i);
    }
}
 
cs

 

$O(\sqrt{N})$

1
2
3
4
5
6
7
8
9
10
11
12
set<int> s;
for (int i = 1; i*<= n; i++) {
    if (n%i == 0) {
        s.insert(i);
        s.insert(n / i); // 10%2를 생각해보면 2와 5가 약수이니까 한번에 처리해주는것
                         // set으로 처리하는 이유는 4%2를 생각해보면 2가 두번 들어갈 수 있음
    }
}
for (auto e : s) {
    printf("%d ", e);
}
 
cs

 

특정 수 하나의 약수를 구하는게 아니라, 여러수의 약수를 동시에 구할때는 에라토스테네스의 채처럼 접근해야 복잡도가 훨씬 작아짐을 발견했다. 이 문제를 풀면서 발견할 수 있었고, 아래 코드는 해당 문제의 답변이기도 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define IN(n) int n;cin>>n
#define OUT(x) cout << (x) << endl
 
int32_t main(){
    const int MAX = 1000000;
    vector<int> f(MAX+1,0);
    for (int i = 1; i <= MAX; i++) {  // i를 약수로 가지는 애들은 모두 찾겠다!
        for (int j = 1; i * j <= MAX; j++)  // 범위내 i의 배수를 모두 구해서
            f[i * j] += i;  // i를 다 더해줌
    }
    vector<ll> g(MAX+1,0);
    for (int i = 1; i <= MAX; i++) {
        g[i] = g[i-1+ f[i];
    }
    IN(T); while (T--) {
        IN(N); OUT(g[N]);
    }
    return 0;
}
cs

 

반응형

stl queue와 동일하게 #include <queue>를 해주고 다음처럼 코딩하면..

1
2
3
4
5
6
7
priority_queue<int> qq;
qq.push(2);
qq.push(5);
qq.push(1);
while (qq.empty() == 0) {
    printf("%d ", qq.top()); qq.pop();
}
cs


5,2,1 순으로 표시된다.

queue에 넣는 순서가 아니라 값 기준으로 정렬되면서 들어가는 것이다.

queue와의 API적인 차이점은 front()대신 top()이 쓰인다는 점


이렇게 되는데는 max heap과 관련이 있다.


내림차순이 아닌 오름차순으로 정렬하려면 다음처럼 queue선언을 바꿔주면 된다.

1
2
priority_queue <intvector<int>, greater<int> > qq;
 
cs

필요이상으로 복잡해진점은 그냥 그러려니 하고 넘어가자--;; C++은 이런 측면으론 결함언어이다.

만약 큐에 들어가는 값이 0보다 크다는것이 보장된다면 위처럼 복잡한 선언을 해주는대신 음수값을 넣어주는 방법도 있다.


이걸 쓸경우 queue하나에 넣고 빼는데 O(log N)으로 생각하면 된다.

즉 N루프안에 priority_queue연산을 넣어도 O(N log N)정도에 돌릴 수 있다는 이야기

O(N^2)가 안될때 고려해볼 만 하다.


관련문제 리스트

코드포스 Playlist


반응형

LCS관련 백준문제를 참고 하자.

재귀풀이

memoize 하지 않으면 O(2^n)복잡도라 n이 20 이상일 경우 TLE 나기 쉽다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <bits/stdc++.h>
using namespace std;
 
char s1[1001],s2[1001];
 
int lcs(char* s1, char* s2)
{
    if(*s1==0 || *s2==0return 0;
    if(*s1==*s2) return 1+lcs(s1+1, s2+1);
    return max(lcs(s1+1,s2),lcs(s1,s2+1));
}
 
int main(void)
{
    scanf("%s %s", s1, s2);
    printf("%d\n",lcs(s1,s2));
    return 0;
}
cs



재귀+Memoization = (Recursive DP)

위의 소스코드에 memoize만 추가해준 코드로 왠만한 문제는 이정도 선에서 푸는게 가장 쉽다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char s1[1001],s2[1001];
int n1, n2;
int dp[1001][1001];
 
int lcs(int i, int j)
{
    if(i==n1 || j==n2) return 0;
    if(dp[i][j]) return dp[i][j];
    if(s1[i]==s2[j]) return dp[i][j] = 1 + lcs(i+1, j+1);
    return dp[i][j] = max(lcs(i+1,j), lcs(i,j+1));
}
 
int main(void)
{
    scanf("%s %s", s1, s2);
    n1 = strlen(s1), n2 = strlen(s2);
    printf("%d\n", lcs(0,0));
    return 0;
}
cs



iterative DP

for문으로 푸는 것에 관심있다면, 조금 덜 직관적일 수 있으며, 아래처럼

dp[i][j] = i,j번째 문자열까지의 lcs 로 놓고.. 테이블을 그려놓고 규칙을 발견해서 푸는게 좋다.

숫자가 증가하는 모서리 부분을 보면 dp[i][j] = 1 + dp[i-1][j-1] 규칙을 찾을 수 있고,

숫자가 증가하는 모서리가 아닌 기타 부분을 보면 dp[i][j] = max(dp[i][j-1], dp[i-1][j]) 규칙을 찾을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bits/stdc++.h>
using namespace std;
 
char s1[1001],s2[1001];
int n1, n2;
int dp[1001][1001];
 
int main(void)
{
    scanf("%s %s", s1, s2);
    n1 = strlen(s1), n2 = strlen(s2);
 
    dp[0][0= s1[0]==s2[0];
    for(int i=1;i<n1;i++) dp[i][0= max(dp[i-1][0], int(s1[i]==s2[0]));
    for(int j=1;j<n2;j++) dp[0][j] = max(dp[0][j-1], int(s1[0]==s2[j]));
 
    for(int i=1;i<n1;i++){
        for(int j=1;j<n2;j++){
            if(s1[i]==s2[j]) dp[i][j] = 1 + dp[i-1][j-1];
            else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
    }
    printf("%d\n", dp[n1-1][n2-1]);
    return 0;
}
cs






반응형

 

 

c++에서 string 입력받기

이 문제에서 처럼, 공백이 포함된 한 줄 전체를 string에 받아올 일이 있을때는 아래처럼 getline을 쓰면 된다.

1
2
string str;
getline(cin, str);
cs

 

c++로 입출력하기

앞에거는 printf,scanf랑 페어링 끄기, 뒤에거는 입출력반복될때 처리에 대한것

1
ios::sync_with_stdio(0);cin.tie(0);
cs

이거 하고 나면 printf, scanf쓰면 안된다.

헤더파일 단순화 하기

알고리즘 문제를 풀다보면 다음과 같이 헤더파일이 점점 길어지고 지저분해짐을 알 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <algorithm>
#include <cmath>
#include <vector>
#include <string>
#include <set>
#include <stack>
#include <queue>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <map>
 
using namespace std;
 
int dp[2][1000000+1];
int main(void)
{
cs

 

그 이유는 매번 문제마다 필요한 vector, map, string등을 그때 그때 추가하다보면 귀찮아서져 그냥 슈퍼셋으로 들고가게 되는 현상이 나타난다(...)

 

근데 gcc에서만 지원하긴하지만 다음처럼 <bits/stdc++h>를 쓰면 헤더파일이 무척 깔끔해진다. 자주쓰는 헤더들을 모아서 처리해주는 역할을 해주기 때문이다.

 

1
2
3
4
5
6
7
#include <bits/stdc++.h>
using namespace std;
 
int main(void)
{
    return 0;
}
cs

 

표준은 아니라서 visual c++나 xcode에서는 잘 안되는데,

xcode 기반의 clion을 쓰는 내 경우는 여기 링크에 나온대로 stdc++.h파일을 복사한다음에 다음경로에 설정해주니 잘 되었다.

만약에 bits/stdc++.h의 경로를 못찾겠다고 나오면,

CMakeLists.txt를 열어서 다음 부분을 추가해주면 되었다(include_directories 부분)

 

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1

mac에서 그냥 c++을 쓰는경우 (visual studio code등에서 사용) 다음 경로에 설정해주면 된다.

 

/usr/local/include

 

visual studio를 쓰는 경우는 아래 링크에 복사

 

D:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.31.31103\include

 

그래프 그려주는 툴

https://csacademy.com/app/graph_editor/ 정말 짱인거 같다.

 

stl을 printf-style로 디버깅하기

visual studio가 라인디버깅 성능이 막강하긴 하지만, PS를 하다가 그렇게 하다보면 시간이 너무 많이 흘러가게 된다.

따라서 간단하게 vector나 map등을 print로 찍어보면 좋은데, built-in으로는 그러한 함수가 없고 반드시 루프를 돌려야 한다.

여기 있는 헤더를 include하면 그러한 점을 보완해준다.

 

간단하게 사용법을 보려면 다음 예제를 보자.

1
2
3
4
5
6
7
8
9
#include <bits/stdc++.h>
#include "dbg.h"  // https://github.com/sharkdp/dbg-macro
using namespace std;
int main()
{
    map<intstring> m = { {1,"abc"}, {2,"cde"} };
    dbg(m);
    return 0;
}
cs

 

dbg()로 감싸주기만 하면 그내용을 다음처럼 출력해준다.

1
[..\nothing\nothing.cpp:7 (main)] m = {{1"abc"}, {2"cde"}}
cs

 

물론 온라인저지에 이대로 제출할수는 없기 때문에(커스텀 헤더라, 저지 서버에서는 인식못하고, 속도도 떨어뜨릴거고)

다음과 같이 감싸주는 부분이 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <bits/stdc++.h>
using namespace std;
#ifdef WIN32
#include "dbg.h"  // https://github.com/sharkdp/dbg-macro
#else
#define dbg(...)
#endif
int main()
{
    map<intstring> m = { {1,"abc"}, {2,"cde"} };
    dbg(m);
    return 0;
}
cs

 

나는 간단하게 WIN32으로 감싸주었지만, 좀 더 고민해서 잘 감싸는게 필요할지도 (-DONLINE_JUDGE등 사용)

 

 

입력개수가 명시되지 않은 입력을 받는법

예를 들어 이 문제의 경우 입력개수 없이 받도록 되어 있다. 

while(cin << n) 이렇게 받는걸로 추천되어 있다.

c스타일로 받을경우는 다음처럼 하면 된다.

1
2
3
4
int n;
while(scanf("%d"&n)!=EOF){
    //do something
}
cs

이 문제에서 연습할 수 있다.

 

freopen 사용하기

백준에서는 ONLINE_JUDGE를 define하고 있으므로 다음과 같이 사용하면 파일로 입력을 줄 수 있다.

1
2
3
#ifndef ONLINE_JUDGE
    freopen("input.txt""rt", stdin);
#endif
cs

 

char를 입력받는 방법

이 문제를 보면 숫자를 입력받은 다음에 char를 입력받게 되어 있는데 생각없이 scanf("%d") 한다음에 scanf("%c")를 하게 되면 \n char 때문에 제대로 안된다. 방법은 scanf("%c") 대신에 scanf("\n%c")로 해주면 되긴했다.

또는 숫자는 scanf("%d") 로 받고 그다음은 scanf("%s")로 문자열로 받아서 문자열 파싱을 해도 됨

 

EOF까지 입력받기

이 문제와 같이 N이주어지는게 아니라 단순히 EOF까지 입력을 받으라는 경우가 있다.

이때는 C언어의 경우는 다음처럼 scanf의 리턴값이 EOF임을 체크하면 된다.

#include <cstdio>

int main() {
    int n;
    while (scanf("%d", &n) != EOF) {
        // process the input
    }
    return 0;
}

 

개행문자까지 문자열을 읽는 방법

3 @ %
10.4 # % @
8 #

위와 같이 어쩔때는 문자가 2개 (@ X), 어쩔때는 문자가 3개 (# % @), 어쩔때는 문자가 한개(#) 주어질때,

아래처럼 %[^\n]를 쓰면 개행문자까지 문자열을 읽을 수가 있다.

double A;
char s[10] = {};
scanf("%lf %[^\n]", &A, s);
for (int i = 0; s[i] != '\0'; ++i) {
    if (s[i] == '@') A *= 3;
    else if (s[i] == '%') A += 5;
    else if (s[i] == '#') A -= 7;
}

이 문제를 참조하자.

반응형

'Programming > Problem Solving' 카테고리의 다른 글

C++ bigint class  (0) 2020.03.30
백준 9663번 N-Queen  (0) 2020.03.15
행렬코딩  (0) 2020.03.01
C언어에서 표준입력으로 string 입력 받기  (0) 2020.02.21
백준 제출시 오류유형 정리  (0) 2019.03.18


런타임오류

1. 배열 크기가 너무 작게 잡혔거나


2. 다음처럼 T로 여러 테스트 케이스를 돌리는데 배열이나 변수가 그 밖에 선언되어 있는 경우 발생가능

1
2
3
4
5
6
7
8
 
int t[1001],pre[1001],r[1001];
vector<int> v[1001];
int main(void)
{
    int T;scanf("%d",&T);
    while(T--){
        
cs


반응형

'Programming > Problem Solving' 카테고리의 다른 글

C++ bigint class  (0) 2020.03.30
백준 9663번 N-Queen  (0) 2020.03.15
행렬코딩  (0) 2020.03.01
C언어에서 표준입력으로 string 입력 받기  (0) 2020.02.21
백준 팁모음  (0) 2019.03.18

이항계수 ${n}\choose{k}$는 $_nC_k$ 조합과 동일하다.

구하는 방법은 다음과 같다.

$$_nC_k = {n!\over{k!(n-k)!}}$$

예를 들어 바구니에 든 10개의공 중에서 순서에 상관없이 3개의 공을 꺼내는 경우의 수를 구하라고 하면 다음처럼 구하면 된다.

$$_{10}C_k = {{10}!\over{3!7!}} = {{{10}\times 9\times 7}\over{1\times 2\times 3}} = 120$$


브루트포스

이를 브루트포스로 구현하면 다음과 같이 되는데
    long long fact[100] = {};
    fact[0] = fact[1] = 1;
    for(int i=2;i<100;i++)fact[i] = i*fact[i-1];
    int n = 10, k = 3;
    printf("%lld\n", fact[n]/(fact[k]*fact[n-k]));



일단 중간에 들어가는 factorial자체가 다음과 같이 n이 20을 넘어가면 long long 범위를 초과해 버려서 n이 20이하일 때만 사용할 수 있는 방법이다.


fact[1]:1
fact[2]:2
fact[3]:6
fact[4]:24
fact[5]:120
fact[6]:720
fact[7]:5040
fact[8]:40320
fact[9]:362880
fact[10]:3628800
fact[11]:39916800
fact[12]:479001600
fact[13]:6227020800
fact[14]:87178291200
fact[15]:1307674368000
fact[16]:20922789888000
fact[17]:355687428096000
fact[18]:6402373705728000
fact[19]:121645100408832000
fact[20]:2432902008176640000
fact[21]:-4249290049419214848


$O(n^2)$ 파스칼의 삼각형 점화식 이용

다음처럼 dp solution을 적용하면 n이 대략 50근방일 경우 해결 가능하다.

(n이 100이 넘어서 예를 들어 $_{100}C_{50}$만 되어도 값이 100891344545564193334812497256 이렇게 커져서 long long 범위를 초과해 버린다.

이럴 경우 이 소스코드를 참고해서 bigint를 쓰면 쉽게 해결할 수도 있다)

const int MAX = 50;
long long C[MAX+1][MAX+1] = {};
int main(void)
{
    C[0][0]=1;
    for(int i=1;i<=MAX;i++){
        C[i][0]=1;
        for(int j=1;j<=MAX;j++)
            C[i][j] = C[i-1][j-1]+C[i-1][j];
    }

    printf("%lld\n", C[10][3]);
    return 0;
}

위 코드를 이해하려면 다음 관계 식을 알아야 하는데,

$$_nC_k=_{n-1}C_{k-1}+_{n-1}C_k$$

수학적으로 증명하는건 오히려 어렵지 않고, 개념적으로 이해하기는 조금 까다로울 수 있는데.. 대부분의 인터넷자료에서 설명하기로는 다음과 같다.


특별한공을 하나 마킹하고, 이공을 포함하는 경우와 포함하지 않는 경우로 나눠서 생각하면

정답은 (포함하는경우)+(포함하지 않는 경우)가 될 것이다.


포함하는 경우에는 마킹한거는 이미 공을 골라놓은 셈이니

n-1개에서 k-1개를 뽑는게 될테니까 $_{n-1}C_{k-1}$이 될 것이고,


포함하지 않은 경우에는 마킹한거를 빼놓고 k개를 골라야 하니 

n-1개에서 k개를 뽑는게 될테니까 $_{n-1}C_k$가 될 것이다.


뭐 적당히 납득할만한 설명인것 같다는 생각은 든다.



$O(n\log n)$ 페르마의 소정리

n의 범위가 더 커질 경우, 보통 modulo 연산과 함께 나오는 경우가 많다.

예를 들어 이 문제와 같이 N이 매우클때 결과값을 10,007로 나눈 나머지를 구하라는 문제가 있다고 하자.


mod연산의 경우 덧셈, 뺄셈, 곱셈에 대해서는 자유롭게 중간 중간 mod를 취해줘도 결과가 같지만 나눗셈에 대해서는 그렇지 않은데,

하필이면 이항계수 계산식에는 나눗셈이 들어가며 그 식을 한줄로 나타내 보면 다음과 같다.

$$ [n! / (k!(n-k)!)]  \% 10,007$$

그런데 만약 $(k!(n-k)!)$의 역원인 $(k!(n-k)!)^{-1}$을 구하게 되면 다음처럼 나눗셈이 곱셈으로 바뀌어서 mod연산을 취할 수 있게 된다.

$$[n! \times (k!(n-k)!)^{-1}]  \% 10,007 = [n!\%10,007 \times (k!(n-k)!)^{-1}\%10,007]  \% 10,007 $$

근데 언뜻들으면 말장난처럼 보일것이다. 왜냐하면 저 역원은 당연히 1보다 작은 분수처럼 보이기 때문이다.

하지만 실제 역원이 아닌 모듈러 연산에 대한 역원은 분수가 아닐 수 있다. 다음을 보자.

$$k!(n-k)! \times x \equiv 1(\mod 10,007)$$

위 식에서 x가 바로 k!(n-k)!의 모듈러 연산에 대한 역원이 되는데, k!(n-k)!에다가 어떤 자연수를 곱하고 모듈러 한 이후에 나온 나머지가 1이기만 하면 되는 조건이기 때문에 충분히 분수가 아니라 자연수가 될 수 있음을 간파할 수 있다.


그럼 분수가 아닌 자연수인 역원은 어떻게 구할 수 있을까?


페르마의 소정리는 다음과 같다.

p가 소수이고 a가 p가 서로소이면

$$a^{p-1} \equiv 1(\mod p)$$


이를 좀 변형하면

$$a \times a^{p-2} \equiv 1(\mod p)$$

이렇게 되고, 신기하게도 a의 모듈러 연산에 대한 역원이 바로 $a^{p-2}$가 됨을 알 수 있다.

$a^{p-2}$를 빠르게 구하는 방법은 분할정복을 쓰면 되며, 나눗셈이 들어가던 원래의 식은 다음처럼 바뀐다.

$$ [n! / (k!(n-k)!)]  \% 10,007 = [n! \times (k!(n-k)!)^{-1}]  \% 10,007 = [n! \times (k!(n-k)!)^{p-2}]\%10,007$$

식을 보면 -1승을 p-2승으로 바꿔주면 되는걸 볼 수 있다.

위의 내용을 전체적으로 반영한 코드는 다음과 같다.


#define ll long long
ll f[4000001];
ll M = 1000000007;
ll pp(ll a, ll p, ll M)
{
    if(p==1) return a;
    ll b = pp(a,p/2,M);
    if(p%2==0){
        return (b*b)%M;
    }else{
        return (((b*b)%M)*a)%M;
    }
}
int main(void)
{
    int N,K;scanf("%d %d",&N, &K);
    f[0]=f[1]=1;for(int i=2;i<4000001;i++)f[i]=(i*f[i-1])%M;
    printf("%lld\n", (f[N]*pp((f[K]*f[N-K])%M,M-2,M))%M);

    return 0;
}


약분하기

modulo가 없고 n의 값이 큰 경우에 사용할 수 있는 방법인데 이것도 재밌는 방법이다.

$$_{10}C_{3} = {{10\times9\times8}\over{1\times2\times3}}$$ 

이런식이 되는데..


10곱하고 1로 나누고 9곱하고 2로나누고 8곱하고 3으로 나누고 이런식으로 구하자는 것 ㅋ

한가지 걱정되는것은 이렇게 했을때 순간적으로라도 나눈값이 정수가 아닌 경우가 생기면 망하는건데

증명은 모르겠지만 실제로 그런경우는 없나보다.


코드는 다음처럼 심플하다.

        int n,m;scanf("%d %d",&n, &m);

        m = min(m,n-m);
        long long r = 1;
        long long div = 1;
        while(m--)
        {
            r*=n--;
            r/=div++;
        }
        printf("%lld\n", r);

n의 범위가 커도 동작하는 대신 쿼리한번에 걸리는 시간이 좀 있다보니 일반적인 경우는 n이 크지만 않다면 dp솔루션이 더 활용도가 높다.












반응형

최대공약수(gcd)를 쉽게 구할 수 있게 해주는 공식.

 

숫자 a와 b가 주어진다고 했을때 두수의 최대 공약수 gcd(a,b)를 구하는 문제를 생각해보자.

 

a>b 라고 했을 때 $a = bq+r$ 꼴로 표현할 수 있는데,

$gcd(a, b) = gcd(b, r) $

이란것이 유클리드 호제법이다.

r = a%b는 b보다 작으므로 gcd(a,b)에서 gcd(b,r)로 순서가 바뀜에 주의하자.

(사실 gcd는 순서에 무관하나 이해를 위해 a>b라는 속성을 유지하는 것)

증명은 여기를 참고하자.

 

계속 위처럼 하다가 r==0이 되는 순간 즉 a가 b로 나눠떨어지는 순간 b를 리턴하면 된다.

 

위의 내용대로 구현한 코드는 다음과 같다.

int gcd(int a, int b) 
{
    while(1)     
    {         
    	int r = a%b;         
        if(r==0) return b;         
        a=b,b=r;     
    } 
}

 

그런데 위의 코드는 함수 종료부분에 return 코드도 없고 while(1)인것도 좀  그래서 살짝 정리하고 나면 인터넷에 많이 퍼져있는 다음 형태가 된다.(자료형도 좀 더 사용성 좋도록 long long으로 변경)

 

long long gcd(long long a, long long b) {
    while (b > 0)     
    {         
    	long long t = a % b;         
        a = b, b = t;     
    }     
    return a; 
}

 

재귀호출 버전

1
2
3
4
long long gcd(long long a, long long b) {
    return (b == 0) ? a : gcd(b, a%b);
}
 
cs

 

 

여러수의 최대공약수 구하기

n개의 숫자에 대한 최대 공약수를 구할때는 단순히 루프를 돌면서 gcd를 반복하면 된다.

마치 max함수가 n개에 대해서 할때는 여러번 적용하듯이..

 

관련문제

http://codeup.kr/problem.php?id=4016

내 풀이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int gcd(int a, int b)
{
    int c;
    while (b) {
        c = a % b;
        a = b, b = c;
    }
    return a;
}
int main(void)
{
    int n = 3;
    int* d = new int[n + 1](); for (int i = 0; i < n; i++scanf("%d", d + i);
    int v = gcd(d[0], d[1]);
    for (int i = 2; i < n; i++) v = gcd(v, d[i]);
    printf("%d\n", v);
    return 0;
}
cs

 

반응형

+ Recent posts