문자열 - 아호 코라식(Aho-Corasick)
아호 코라식(Aho-Corasick)에 대해 알아보자.
2060 염소줄서기 풀이 및 코드
오랜만에 다이아 문제 풀이를 써보려고 한다. 이 문제는 내가 옛날에 북마크 해뒀던 문젠데 북마크에서 거의 1년간 썩어가고 있어서 속상해서 선택했다. 문제 이해가 어렵지 않아서 선택한 것도 있다. 다이아 문제를 하루에 하나씩 풀면 엄청난 도움이 되겠지만 블로그 풀이를 안보면 하루에 하나는 무슨 일주일에 하나도 힘들것이다. 그래도 1일 1플레는 도전을 해봐야겠다.
염소들 번호가 있는데 밥먹는 순서는 다음과 같다. 염소번호를 이진수로 나타냈을 때
이진수로 A부터 B까지의 염소가 있을 때, k번째 밥을 먹는 염소는 몇 번 친구인가?
당연히 정렬을 생각할 수 있다. 하지만 1의 개수가 최대 31개, 따라서 integer형 범위를 모두 포함한다면 약 20억이다. $A = 0$, $B = 2^{32} - 1$ 이라면 절대 순수하게 정렬할 수는 없을 것이다.
손으로 하는 풀이 내가 내 눈과 손으로 답을 구할 때는 먼저 1의 개수가 작은 것부터 보면서 counting을 했다. 1의 개수가 1개인것이 $a_1$개 2개 인것이 $a_2$ 개 .. 이런식으로 생각하면서 k를 줄여나갔다.
세 단계로 풀이를 세웠다.
rnk(i, X) : X보다 작은 1의 개수가 i개인 수의 개수
A와 B가 고정이기 때문에 rnk(i, B) - rnk(i, A-1)을 하면 i별로 1의 개수가 i개인 염소의 수를 구할 수 있다.
예를 들어 1001011보다 작은 1의 개수가 5개인 수들을 구해보자! 아래 예시에서 첫번째 요소를 볼때 0이 오는지 1이 오는지에 따라 경우의 수를 세줄 수 있다.
즉, 1을 만났을 때 $\binom{이후 자리수}{남은 1의 수}$ 를 더해주면 된다.
ll rnk(int cnt, string s){
ll ret = 0, one = 0;
for(int i=0;i<s.length();i++){
if (s[i] == '1') one++;
}
for(int i=0;i<s.length();i++){
if ((s.length() - i == one) || one == 0 || cnt == 0){
ret += binom(one, cnt);
break;
}
if(s[i] == '1'){
ret += binom(s.length()-i-1, cnt);
one -= 1;
cnt -= 1;
}
}
return ret;
}
이렇게 rnk함수를 구할 수 있었다. binom함수는 2차원 dp로 구현했다. 처음에 one == 0, cnt == 0이 되는 경우를 처리하지 않아서 계속 틀렸다.
누적합을 이용하면 $O(31)$ 이내에 간단하게 몇개의 비트를 쓰는지 확인할 수 있다.
for(int i=0;i<32;i++){
dp[i] = ((i == 0) ? 0 : dp[i-1]) + dp[i];
}
int c = 0;
ll kk = 0;
for(int i=0;i<32;i++){
if(dp[i] >= k) break;
c = i;
kk = dp[i];
}
k -= kk;
k번째 수가 c개의 비트를 가지고 있다는 뜻이고 kk는 지금까지 건너뛴 염소들의 수다. 이제 A와 B사이에 있는 c개의 비트를 가진 수들 중에서 k(-= kk)번째 수를 구하면 된다.
사실 이 과정에서 막혔다… c개의 비트를 가진 수들 중에서 k번째 수를 어떻게 구할까..? 근데 잘 생각해보니 그냥 아까의 rnk함수를 이용해서 이분탐색을 하면 됐다. 왜냐면 x보다 작은 1의 개수가 c인 수는 정렬되어있기 때문이다. 그리고 그것이 변하는 순간은 무조건 c개의 비트를 가지고 있는것이 명확하기 때문에 제일 먼저 k가 나오는 순간을 포착하면 답이 된다.
#include <bits/stdc++.h>
#define fast_io cin.tie(NULL); ios_base::sync_with_stdio(false);
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tiii;
typedef tuple<ll, ll, ll> tlll;
#define xx first
#define yy second
ll comb[32][32];
ll binom(int n, int k){
if(n < k) return 0;
if(k == 0 || n == k) return 1;
ll &ret = comb[n][k];
if(ret != -1) return ret;
ret = binom(n-1, k) + binom(n-1, k-1);
return ret;
}
ll rnk(int cnt, string s){
ll ret = 0, one = 0;
for(int i=0;i<s.length();i++){
if (s[i] == '1') one++;
}
for(int i=0;i<s.length();i++){
if ((s.length() - i == one) || one == 0 || cnt == 0){
ret += binom(one, cnt);
break;
}
if(s[i] == '1'){
ret += binom(s.length()-i-1, cnt);
one -= 1;
cnt -= 1;
}
}
return ret;
}
string to_binary(ll x){
if(!x) return "0";
string ret = "";
while(x){
ret += (x % 2) + '0';
x /= 2;
}
reverse(ret.begin(), ret.end());
return ret;
}
ll to_ll(string s){
if(s == "-1") return -1;
ll ret = 0;
ll j = 1;
for(int i=s.length()-1;i>=0;i--){
ret += j * (s[i] - '0');
j *= 2;
}
return ret;
}
int main(){
fast_io
string A, B;
cin >> A >> B;
ll k; cin >> k;
assert (to_ll(B) - to_ll(A) + 1 >= k);
memset(comb, -1, sizeof(comb));
if(A != "0"){
for (int i = A.length() - 1; i >= 0; i--){
if (A[i] == '0') A[i] = '1';
else{
A[i] = '0';
if(i == 0){
if(A.length() != 1) A.erase(A.begin());
}
break;
}
}
}else{
A = "-1";
}
vector<ll> dp(32, 0);
for(int i=0;i<32;i++){
dp[i] = rnk(i, B) - (A == "-1" ? 0 : rnk(i, A));
}
for(int i=0;i<32;i++){
dp[i] = ((i == 0) ? 0 : dp[i-1]) + dp[i];
}
int c = 0;
ll kk = 0;
for(int i=0;i<32;i++){
c = i;
if(dp[i] >= k) break;
kk = dp[i];
}
k -= kk;
ll lo = to_ll(A), hi = to_ll(B);
while(lo + 1 < hi){
ll mid = (lo + hi) >> 1;
if(rnk(c, to_binary(mid)) - (A == "-1" ? 0 : rnk(c, A))< k) lo = mid;
else hi = mid;
}
cout << to_binary(hi);
}
솔직히 엄청 어려운 알고리즘은 하나도 안들어간다. 진법 변환, 자릿수 dp, 이항계수, 누적합 등 매우 쉬운 알고리즘 들이다. 항상 보면 이런 자잘한 것들이 모여서 어려운 문제를 만들어낸다..ㅠㅠ 이 문제의 포인트는 누적합 논리인것 같다. 그리고 누적합에서는 이분탐색이 가능하다는 것도 잘 이용한 좋은 문제라고 생각한다.
아호 코라식(Aho-Corasick)에 대해 알아보자.
docker를 이해해보자
2060 염소줄서기 풀이 및 코드
분할정복을 이용한 다이나믹 프로그래밍 최적화
코드포스 다시 열심히! 블로그도 열심히!
rust 공부시작!
코드포스 블루 달성 후기
여름캠프 및 SUAPC 후기
2023-05-25-Edu Codeforce round 149 (Div.2)
2023-05-13-Edu Codeforce round 148 (Div.2)
HLD(Heavy Light Decomposition)
행렬 거듭 제곱
2023-05-02-It takes two
Codeforce round 868 (Div.2)
2월 11일 문제풀이
Codeforces#846, TypeDB Foreces 2023, Codeforces#848 업솔빙
ps5 게임 : 용과같이 제로 리뷰
백준 23877번 Convoluted Intervals 문제풀이
codeforce round #828(div 3), EDU #137(div 2) 업솔빙
codeforce round #823(div 2), #824(div 2) 업솔빙
AtCoder Beginner Contest 270 업솔빙
ps5 게임 : 용과같이 극 1 리뷰
백준 18719번 Binomal 문제풀이
백준 14288번 회사문화4 문제풀이
백준 3308번 Matching 문제풀이
백준 18186번 라면사기(large) 문제풀이
백준 4196번 도미노 문제풀이
백준 3176번 도로 네트워크 문제풀이
백준 16367번 TV Show Game 문제풀이
ps5 게임 : 페르소나 5 더 로열 리뷰
codeforce round #811(div 3), #812(div 2), CodeTon round 2 업솔빙
백준 21162번 뒤집기 K 문제풀이
codeforce round #808(div 2), #803(div 2, virtual) 업솔빙
codeforce round #807(div 2) 업솔빙
백준 10167번 금광 문제풀이
codeforce round #805(div 3), #806(div 4) 업솔빙
백준 18253번 최단경로와 쿼리 문제풀이
에듀 라운드 131 업솔빙
백준 1949번 우수 마을 문제풀이
백준 3665번 최종 순위 문제풀이
Trie 자료구조 이해하기
merge sort를 이용하여 inversion 개수세기
3솔의 벽이 너무 높다..
lazy propagation없이 구간 갱신하기
코드포스 폭망기념 upsolving
백준 11505 구간 곱 구하기 문제풀이
백준 1305 광고 문제풀이
백준 4386 별자리 만들기 문제풀이
백준 4803 트리
백준 2206 벽 부수고 이동하기 문제풀이
백준 2166 다각형의 면적 문제풀이
백준 12015 가장 긴 증가하는 부분 수열2 문제풀이
백준 10986 나머지합 문제풀이
스택 구현하기 ========== 자료구조의 기본이라고 하면 스택 과 큐가 있다 백준 10828번에서 마주친 스택
매우매우 많은 정렬이 있지만 그중 가장 안 어려운 3가지를 공부해보았다.
Leave a comment