cubelover의 블로그

Tree Optimization

...2018. 8. 12. 18:38

트리에서 사용할 수 있는 특수한 몇몇 알고리즘들은 실제 시간복잡도가 생각한 것보다 작을 수 있다. 이 글에서는 그런 몇몇 케이스를 다룬다.


~


노드 개수가 \(A\)인 서브트리의 결과와 노드 개수가 \(B\)인 서브트리의 결과를 \(O(AB)\)의 시간에 합칠 수 있는 경우


최악의 경우 \(A\)와 \(B\)가 모두 \(O(N)\), 합치는 횟수가 \(O(N)\)이므로 \(O(N^3)\)인 것 같지만, 사실 \(O(N^2)\)이다.


~


증명


노드 개수가 \(N\)인 서브트리의 결과를 만드는 데 필요한 시간을 \(T(N)\)이라고 정의하자.


또한, 노드 개수가 \(A\)인 서브트리의 결과와 노드 개수가 \(B\)인 서브트리의 결과를 합치는 데 \(cAB\) (\(c\)는 상수) 이하의 시간이 필요하다고 하자.


\(N<A+B\)인 모든 \(N\)에 대해 \(T(N) \le cN^2\)이라고 가정하면,


\[T(A+B) \le T(A) + T(B) + cAB \le cA^2 + 2cAB + cB^2 = c(A+B)^2\]


따라서 수학적 귀납법에 의해 시간복잡도가 \(O(N^2)\)이 된다.


~


응용


이 아이디어로 ACM-ICPC Daejeon Regional 2012 J번(Slicing Tree), Coder's high 2016 Round 2: Nexon Arena C번(트리의 변화), shake! 2017 F번(단신쓴짠)을 풀 수 있다.


~


루트가 있는 트리에서, 각 노드마다 (자식들 중 높이가 두 번째로 높은 노드의 높이 + 높이가 세 번째로 높은 노드의 높이 + ... + 높이가 가장 낮은 노드의 높이)의 합으로 문제를 풀 수 있는 경우


이 경우 시간복잡도가 \(O(N)\)이다.


~


증명


노드 \(u\)의 높이를 \(h_u\)라고 하자. 높이의 정의에 따라, 노드 \(u\)의 자식들 \(v\)에 대해 \(h_u = \max_v \{h_v\} + 1\)이다.


또한, 루트를 제외하고 모든 노드의 부모는 단 한 개이므로, 트리의 루트를 \(r\)이라고 하면 모든 노드에 대해 모든 자식들의 높이 합은 다음과 같다.


\[\sum_{u \neq r} h_u = \sum_u h_u - h_r\]


모든 노드에 대해 두 번째, 세 번째, ...로 높이가 높은 노드의 높이 합은 전체 높이 합에서 첫 번째로 높이가 높은 노드의 높이 합을 뺀 것과 동일하다.


\[\sum_{u \neq r} h_u - \sum_u \max_v \{h_v\} = \sum_u h_u - \sum_u \max_v \{h_v\} - h_r = \sum_u ( h_u - \max_v \{h_v\} ) - h_r = N - h_r = O(N)\]


~


응용


이 아이디어로 가중치 없는 트리에서 세 정점의 순서쌍 (u, v, w)에 대하여, (u-v의 거리) = (v-w의 거리) = (w-u의 거리)를 만족하는 순서쌍이 몇 개나 있는지를 \(O(N)\)으로 구할 수 있다.


이 문제를 \(O(N^2)\)으로 풀면 만점이 나오는 버전이 POI에 출제되었다.


Archive: https://main.edu.pl/en/archive/oi/21/hot

Baekjoon Online Judge: https://www.acmicpc.net/problem/10122


Baekjoon Online Judge에서 \(O(N)\) 풀이로 0ms를 받았으니, 관심이 있는 분은 \(O(N^2)\) 풀이로 맞은 뒤 코드를 읽어보시기 바랍니다.

'...' 카테고리의 다른 글

ONTAK 2010 Day 7 Generator  (0) 2018.07.17
BOJ Achievements  (0) 2018.05.17
Ubuntu PPTP VPN Server  (0) 2017.12.17
Baekjoon Online Judge 13538  (3) 2016.11.03
Baekjoon Online Judge 1659  (0) 2016.02.02

ONTAK 2010 Day 7 Generator

...2018. 7. 17. 04:30

입력으로 정수 i(0 ≤ i ≤ 10)가 들어오면, genzaw.zip의 gen$i.out에 있는 내용을 출력하는 문제이다. 단, 소스코드는 10만자를 넘을 수 없다.


그래서 genzaw.zip을 까 보면, 가장 작은 gen0.out을 제외하고 200KB 정도 되는 파일부터 3MB 정도 되는 파일까지 다양하다. 물론 이걸 그대로 출력할 수는 없고, 파일 형태를 보고 생성한 알고리즘을 유추해서 문제를 풀어야 한다.


각 파일마다 푸는 방법이 많이 다르므로, 파일마다 힌트와 풀이를 정리해서 올린다.


~


gen0.out


예제 출력에 해당한다.




~


gen1.out


알파벳이 적힌 파일이다.




~


gen2.out


수열이 적힌 파일이다.




~


gen3.out


#과 .이 적힌 파일이다.




~


gen4.out


0과 1이 적힌 파일이다.




~


gen5.out


폴란드어가 적힌 파일이다.




~


gen6.out


T[$number]="$string";이 적힌 파일이다.




~


gen7.out


#과 .이 적힌 파일이다.




~


gen8.out


#과 .이 적힌 파일이다.




~


gen9.out


#과 .이 적힌 파일이다.




~


gen10.out


수열과 행렬이 적힌 파일이다.




'...' 카테고리의 다른 글

Tree Optimization  (2) 2018.08.12
BOJ Achievements  (0) 2018.05.17
Ubuntu PPTP VPN Server  (0) 2017.12.17
Baekjoon Online Judge 13538  (3) 2016.11.03
Baekjoon Online Judge 1659  (0) 2016.02.02

BOJ Achievements

...2018. 5. 17. 23:18

NORMAL


100문제 이상 풀기

1000회 이상 제출하기

하루에 10문제 이상 풀기

7일 연속으로 매일 문제 풀기

북마크 기능 사용하기

한 게시글에 좋아요 1개 이상 받기

학교/회사 등록하기

슬랙에서 이야기하기

풀이 작성하기

그룹 만들기

문제 만들기

데이터 추가하기

대회 참가하기

나만 푼 문제 만들기

맞은 사람 목록 맨 위에 올라가기

숏코딩 목록 맨 위에 올라가기

Daily 로또(10948번)에서 맞았습니다 받기

Text로 10문제 이상 풀기


~


HARD


1000문제 이상 풀기

10000회 이상 제출하기

하루에 30문제 이상 풀기
30일 연속으로 매일 문제 풀기

한 게시글에 좋아요 10개 이상 받기

아이디 변경하기

블로그에 글 쓰기

대회 개최하기

스페셜 저지 추가하기

틀렸던 코드 재채점으로 맞았습니다 받기

데이터를 추가해서 맞은 사람 100명 이상 감소시키기

시간제한과 동일한 수행시간으로 맞았습니다 받기

수행시간, 메모리 사용량, 코드 길이 모두 동일한 결과 받기

모든 언어로 맞았습니다 받기

모든 문제에 제출하기

언어 랭킹 1위 달성하기

랜덤 게임~~(10944번)에서 맞았습니다 받기

Text로 50문제 이상 풀기


~


INSANE


10000문제 풀기

하루에 100문제 이상 풀기

365일 연속으로 매일 문제 풀기

한 게시글에 좋아요 100개 이상 받기

모든 언어로 모든 결과 받기

로또(10947번)에서 100점 받기

랜덤 게임~~~~(10946번)에서 100점 받기

Text로 100문제 이상 풀기

'...' 카테고리의 다른 글

Tree Optimization  (2) 2018.08.12
ONTAK 2010 Day 7 Generator  (0) 2018.07.17
Ubuntu PPTP VPN Server  (0) 2017.12.17
Baekjoon Online Judge 13538  (3) 2016.11.03
Baekjoon Online Judge 1659  (0) 2016.02.02

Ubuntu PPTP VPN Server

...2017. 12. 17. 01:38

pptpd 설치


다음 명령어를 실행한다.


sudo apt-get install pptpd


~


ip 설정


/etc/pptpd.conf 파일의 맨 마지막에 다음을 추가한다.


localip 192.168.0.1
remoteip 192.168.0.100-200


~


DNS 설정


/etc/ppp/pptpd-options 파일의 ms-dns 항목을 주석해제하고 수정한다.


ms-dns 8.8.8.8
ms-dns 8.8.4.4


~


사용자 추가


/etc/ppp/chap-secrets 파일에 사용자를 추가한다. "(아이디) pptpd (비밀번호) (IP)" 형식으로 한 줄에 하나씩 적으면 된다.


cubelover pptpd p4ssw0rd 192.168.0.123
guest pptpd guest *

~


interface 이름 알아내기


다음 명령어를 실행하면 interface가 여러 개 나오는데, 이 중 vpn에 routing할 interface의 이름을 찾는다. 유선랜을 사용하는 경우 보통 eth0지만 아닌 경우도 있다.


ifconfig -a


~


iptables 설정


앞에서 알아낸 interface의 이름을 (interface)에 넣고 실행한다.


iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o (interface) -j MASQUERADE


~


ip forwarding 설정


/etc/sysctl.conf 파일에서 net.ipv4.ip_forward=1 항목을 주석해제한다. 그리고 다음 명령어를 실행한다.


sudo sysctl -p


~


pptpd 재시작


다음 명령어를 실행한다.


sudo pptpd restart


~


UFW 설정 (UFW를 사용하는 경우)


/etc/default/ufw 파일에서 DEFAULT_FORWARD_POLICY를 ACCEPT로 고쳐준다.


DEFAULT_FORWARD_POLICY="ACCEPT"


/etc/ufw/before.rules 파일에서 *filter 앞 부분에 다음을 추가한다.


# nat Table rules
*nat
:POSTROUTING ACCEPT [0:0]

# Allow traffic from clients to eth0
-A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE

# commit to apply changes
COMMIT


그리고 # drop INVALID packets 바로 다음에 다음을 추가한다.


-A ufw-before-input -p 47 -i $iface -j ACCEPT


다음 명령어를 실행한다.


sudo ufw allow 1723
sudo ufw disable
sudo ufw enable


~


여기까지 잘 동작하는 지 확인한다. 잘 동작한다면 재부팅할 때 iptable이 날아가는 것을 방지하기 위해서 저장해뒀다가 부팅할 때 불러오도록 해야 한다.


다음 명령어를 실행한다.


sudo su
iptables-save > /etc/iptables.rules
exit


/etc/rc.local 파일의 exit 0 이전에 다음을 추가한다.


/sbin/iptables-restore < /etc/iptables.rules

'...' 카테고리의 다른 글

ONTAK 2010 Day 7 Generator  (0) 2018.07.17
BOJ Achievements  (0) 2018.05.17
Baekjoon Online Judge 13538  (3) 2016.11.03
Baekjoon Online Judge 1659  (0) 2016.02.02
1 이상 N 이하의 정수에서 i의 빈도수  (0) 2016.01.27

#include <cstdio>
#include <functional>

using namespace std;

template <typename _T>
class IndexedTree {
private:
	size_t m;
	_T *A;
	function<_T(const _T &, const _T &)> F;
public:
	IndexedTree(size_t n, function<_T(const _T &, const _T &)> f) {
		for (m = 1; m < n; m <<= 1);
		A = new _T[m << 1];
		F = f;
	}
	void Update(size_t idx, _T val) {
		idx |= m;
		A[idx] = val;
		while (idx >>= 1) A[idx] = F(A[idx << 1], A[idx << 1 | 1]);
	}
	_T Query(size_t L, size_t R) {
		_T Lval = _T(), Rval = _T();
		L |= m;
		R |= m;
		while (L <= R) {
			if (L & 1) Lval = F(Lval, A[L++]);
			if (!(R & 1)) Rval = F(A[R--], Rval);
			L >>= 1;
			R >>= 1;
		}
		return F(Lval, Rval);
	}
};

int a[10] = { 6, 2, 3, 8, 4, 5, 1, 9, 7, 10 };

int main() {
	IndexedTree<int> IT(10, [](auto u, auto v) { return u + v; });
	for (int i = 0; i < 10; i++) IT.Update(i, a[i]);
	printf("%d %d : %d\n", 2, 6, IT.Query(2, 6));
	printf("%d %d : %d\n", 3, 8, IT.Query(3, 8));
	return 0;
}

'구현' 카테고리의 다른 글

Stable Radix Sort  (0) 2016.10.21

Dividing Polynomials

수학2017. 1. 12. 15:54

다항식의 나눗셈이란, \(x\)에 대한 \(n\)차 다항식 \(f(x)\)와 \(x\)에 대한 \(m\)차 다항식 \(g(x)\)에 대하여, 다음을 만족하는 \(x\)에 대한 \(n-m\)차 다항식 \(q(x)\)와 \(m-1\)차 다항식 \(r(x)\)를 구하는 것을 말한다. (\(n \ge m\))


\[f(x)=g(x)q(x)+r(x)\]


이는 높은 차수부터 하나씩 지워 나가는 것으로 \(O(n^2)\) 시간 안에 행할 수 있지만, 여기서는 Discrete Fourier Transform을 이용하여 \(O(n \log n)\)의 시간에 행하는 방법을 소개한다.


~


먼저, \(x\)에 \(z^{-1}\)을 대입하고, 양변에 \(z^n\)을 곱해보자.


\[z^nf(z^{-1})=(z^m g(z^{-1}))(z^{n-m}q(z^{-1}))+z^{n-m+1}(z^{m-1}r(z^{-1}))\]


여기서 \(z^nf(z^{-1})\), \(z^m g(z^{-1})\), \(z^{n-m}q(z^{-1})\) 그리고 \(z^{m-1}r(x)\)는 \(f(x)\), \(g(x)\), \(q(x)\), \(r(x)\)의 계수 순서를 바꾸고 \(x\) 대신에 \(z\)를 적은 것임을 알 수 있다.


이제부터 이들을 \(F(z)\), \(G(z)\), \(Q(z)\), \(R(z)\)라 하자. 그러면 다음처럼 정리할 수 있다.


\[F(z)=G(z)Q(z)+R(z)z^{n-m+1}\]


여기서 \(Q(z)\)는 \(z\)에 대한 \(n-m\)차 다항식임에 유의하자. 만약 \(G(z)H(z)=1\mod z^{n-m+1}\)을 만족하는 다항식 \(H(z)\)가 존재한다면, 우리는 \(Q(z)\)를 다음처럼 나타낼 수 있다.


\[Q(z)=F(z)H(z) \mod z^{n-m+1}\]


\(H(z)\)를 찾을 수만 있다면 Fast Fourier Transform을 이용하여 \(Q(z)\)를 \(O(n\log n)\)의 시간에 구할 수 있다. 이후 계수 순서만 바꾸면 \(q(x)\)를 구할 수 있다.


또한 \(r(x)=f(x)-g(x)q(x)\)인 점을 이용하여 \(r(x)\)도 \(O(n \log n)\)의 시간에 구할 수 있다.


~


그렇다면 \(G(z)H(z)=1\mod z^{n-m+1}\)을 만족하는 다항식 \(H(z)\)는 어떻게 구할까?


\(g(x)\)의 차수가 \(m\)이라고 했으므로, \(G(z)\)의 상수항은 \(0\)이 아님을 알 수 있다.


따라서 \(G(z)\)의 상수항을 \(c\)라 하면, \(G(z)(1/c)=1 \mod x\)임을 알 수 있다.


\(G(z)U(z)=1 \mod x^k\)을 만족하는 다항식 \(U(z)\)를 알고 있다고 할 때, \(G(z)V(z)=1 \mod x^{2k}\)를 만족하는 다항식 \(V(z)\)는 다음의 꼴을 가진다.


\[V(z)=U(z)+T(z)z^k \mod z^{2k}\]


\(G(z)\)를 \(k-1\)차 다항식 \(G_0(z)\)와 \(m-k+1\)차 다항식 \(G_1(z)\)에 대하여 \(G(z)=G_0(z)+G_1(z)z^k\)꼴로 나타냈을 때 \(G_0(z)U(z)=1+W(z)z^k\)라 하면


\[G(z)V(z)=(G_0(z)+G_1(z)z^k) (U(z)+T(z)z^k)=1+(W(z)+G_0(z)T(z)+G_1(z)U(z))z^k \mod z^{2k}\]

\[W(z)+G_0(z)T(z)+G_1(z)U(z)=0 \mod z^k\]


여기서 우리는 \(G_0(z)U(z)=1 \mod z^k\)임을 알고 있으므로


\[U(z)W(z)+T(z)+G_1(z)U^2(z)=0\mod z^k\]

\[T(z)=-U(z)(W(z)+G_1(z)U(z))\mod z^k\]


이상을 정리하면


\[V(z)=U(z)(1-W(z)z^k-G_1(z)U(z)z^k)=U(z)(2-G_0(z)U(z)-G_1(z)U(z)z^k)=U(z)(2-G(z)U(z))\mod z^{2k}\]


\(k\ge n-m+1\)이 될 때까지 이를 반복하면 \(G(z)H(z)=1\mod z^{n-m+1}\)을 만족하는 다항식 \(H(z)\)를 구할 수 있다.


\(G(z)U(z)\)를 구하는 부분에서 Fast Fourier Transform을 사용하면 \(O(n\log n)\)의 시간에 \(H(z)\)를 구할 수 있다.


~


즉, \(f(x)/g(x)\)를 구하는 과정은 다음과 같다.


1. \(f(x), g(x)\)의 계수 순서를 바꿔 \(F(z), G(z)\)를 만든다.

2. \(G(z)\)의 상수항을 \(c\)라고 할 때, \(H(z)\leftarrow1/c, k\leftarrow1\)로 놓는다.

3. 다음 과정을 \(k\ge n-m+1\)이 될 때까지 반복한다.

3-1. \(k\leftarrow2k\)

3-2. \(H(z)\leftarrow H(z)(2-G(z)H(z)) \mod z^k\)

4. \(Q(z)\leftarrow F(z)H(z) \mod z^{n-m+1}\)

5. \(Q(z)\)의 계수 순서를 바꿔 \(q(x)\)를 구한다.

6. \(r(x)\leftarrow f(x)-g(x)q(x)\)


여기서 모든 다항식 곱셈에는 Fast Fourier Transform을 사용하고, \(\mod z^k\)인 경우 \(k\)차 이상의 항을 무시하고 계산하면 \(O(n\log n)\)의 시간에 나눗셈을 행할 수 있다.

'수학' 카테고리의 다른 글

きたまさ法  (0) 2016.11.24
Discrete Fourier Transform  (0) 2016.11.17
Number Theoretic Fast Fourier Transform  (1) 2016.11.02

きたまさ法

수학2016. 11. 24. 01:51

키타마사법은 다음과 같은 점화식을 가지는 수열의 \(n\)번째 항을 빠르게 계산하는 데 사용된다.


\[a_{n}=\sum_{k=1}^{m}a_{n-k}c_{k}\]


이 점화식을 단순 행렬곱으로 풀면 \(O(m^{3}\log n)\)의 시간이 필요하지만, 키타마사법을 사용하면 \(O(m^{2}\log n)\)의 시간이면 충분하다.


~


임의의 \(a_{n}\)을 다음처럼 \(a_{1}, a_{2}, \dots, a_{m}\)에 대한 식으로 표현할 수 있다.


\[a_{n}=\sum_{k=1}^{m}a_{k}d_{k}\]


키타마사법의 아이디어는, 위 식을 만족하는 \(n\), \(d_{k}\)와 임의의 정수 \(l\)에 대하여 다음이 성립한다는 것이다.


\[a_{n+l}=\sum_{k=1}^{m}a_{k+l}d_{k}\]


이 식이 성립하는 이유는 수학적 귀납법으로 쉽게 보일 수 있으므로 생략한다.


이를 통해 \(a_{2n}\)에 대한 식을 정리하면 다음과 같다.


\[a_{2n}=\sum_{k=1}^{m}a_{n+k}d_{k}=\sum_{k=1}^{m}\sum_{j=1}^{m}a_{j+k}d_{j}d_{k}\]


따라서 \(a_{2n}\)을 \(a_{2}, a_{3}, \dots, a_{2m}\)에 관한 선형식으로 표현할 수 있고, 이들의 계수는 \(O(m^{2})\) 시간 안에 계산할 수 있다.


또한 \(a_{m+1}, a_{m+2}, \dots, a_{2m}\)을 \(a_{1}, a_{2}, \dots, a_{m}\)에 대한 선형식으로 나타내는 데 \(O(m^{2})\)의 시간이면 충분하다.


이를 종합하면, \(a_{n}\)에 대한 식을 \(a_{2n}\)에 대한 식으로 바꾸는 데 \(O(m^{2})\)의 시간밖에 소요되지 않으므로 \(O(m^{2}\log n)\)의 시간복잡도로 \(n\)번째 항을 계산할 수 있다.


#include <cstdio>

const int P = 1000000007;

int a[1001];
int c[1001];
int d[1001];
int t[2002];

void Kitamasa(int n, int m) {
	int i, j;
	if (n == 1) {
		d[1] = 1;
		for (i = 2; i <= m; i++) d[i] = 0;
		return;
	}
	if (n & 1) {
		Kitamasa(n ^ 1, m);
		j = d[m];
		for (i = m; i >= 1; i--) d[i] = (d[i - 1] + (long long)c[m - i + 1] * j) % P;
		return;
	}
	Kitamasa(n >> 1, m);
	for (i = 1; i <= m + m; i++) t[i] = 0;
	for (i = 1; i <= m; i++) for (j = 1; j <= m; j++) t[i + j] = (t[i + j] + (long long)d[i] * d[j]) % P;
	for (i = m + m; i > m; i--) for (j = 1; j <= m; j++) t[i - j] = (t[i - j] + (long long)c[j] * t[i]) % P;
	for (i = 1; i <= m; i++) d[i] = t[i];
}

int main() {
	int i, n, m, r;
	scanf("%d%d", &n, &m);
	for (i = 1; i <= m; i++) scanf("%d", &a[i]);
	for (i = 1; i <= m; i++) scanf("%d", &c[i]);
	Kitamasa(n, m);
	r = 0;
	for (i = 1; i <= m; i++) r = (r + (long long)a[i] * d[i]) % P;
	printf("%d", r);
}

'수학' 카테고리의 다른 글

Dividing Polynomials  (0) 2017.01.12
Discrete Fourier Transform  (0) 2016.11.17
Number Theoretic Fast Fourier Transform  (1) 2016.11.02

Discrete Fourier Transform

수학2016. 11. 17. 14:27

이산 푸리에 변환이란 주기가 \(N\)인 복소수열 \(x_{0},x_{1},\dots,x_{N-1}\)에 대하여 다음과 같이 정의되는 주기가 \(N\)인 복소수열 \(X_{0},X_{1},\dots,X_{N-1}\)로 바꾸는 변환을 말한다.


\[X_{k}=\sum_{n=0}^{N-1}e^{-2\pi ikn/N}x_{n}\]


여기서 \(e^{iz}=\cos z+i\sin z\)이며, 이로부터 \(e^{2\pi i}=e^{-2\pi i}=1\)임을 쉽게 알 수 있다.


~


이산 푸리에 역변환


또한 이렇게 변환된 복소수열 \(X_{k}\)는 다음과 같은 과정을 통하여 복소수열 \(x_{n}\)으로 역변환될 수 있다.


\[x_{n}=\frac{1}{N}\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}\]


~


이산 푸리에 역변환의 증명


\(X_{k}\)의 정의대로 식을 풀어보면 다음과 같다.


\[\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}=\sum_{k=0}^{N-1}\sum_{j=0}^{N-1}e^{2\pi ik(n-j)/N}x_{j}=\sum_{j=0}^{N-1}x_{j}\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}\]


\(j=n\)인 경우에는 다음처럼 된다.


\[\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}=\sum_{k=0}^{N-1}1=N\]


\(j\neq n\)인 경우에는 다음처럼 된다.


\[\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}=\frac{e^{2\pi i(n-j)}-1}{e^{2\pi i(n-j)/N}-1}=0\]


따라서 다음이 성립한다.


\[\frac{1}{N}\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}=\frac{1}{N}Nx_{n}=x_{n}\]


~


이산 푸리에 변환과 Convolution


주기가 \(N\)인 수열 \(a_{n}\)과 \(b_{n}\)을 각각 이산 푸리에 변환한 수열을 \(A_{n}\)과 \(B_{n}\)이라 하자.


또한 새로운 수열 \(C_{n}=A_{n}B_{n}\)을 정의하고, 이를 이산 푸리에 역변환한 수열을 \(c_{n}\)이라 하면 다음과 같은 식이 성립한다.


\[c_{n}=\sum_{k=0}^{N-1}a_{k}b_{n-k}\]


~


이산 푸리에 변환과 Convolution의 증명


\(C_{n}\)의 정의대로 식을 풀어보면 다음과 같다.


\[C_{n}=A_{n}B_{n}=\sum_{j=0}^{N-1}\sum_{k=0}^{N-1}e^{-2\pi i(j+k)n/N}a_{k}b_{j}=\sum_{j=0}^{N-1}e^{-2\pi ijn/N}\sum_{k=0}^{N-1}a_{k}b_{j-k}\]


따라서 이를 역변환한 수열 \(c_{n}\)에 대한 식이 성립한다.


~


고속 푸리에 변환


이산 푸리에 변환을 단순한 반복문으로 계산하면 \(O(N^{2})\)의 시간복잡도를 가지지만, 이를 \(O(N\log N)\)의 시간복잡도에 행할 수 있는 방법이 고속 푸리에 변환이다.


고속 푸리에 변환에는 다양한 알고리즘이 있지만, 여기서는 \(N=2^{M}\) 꼴의 푸리에 변환에 최적화된 Cooley-Tukey 알고리즘에 대해 설명한다.


\(X_{k}\)를 다음처럼 고쳐 적을 수 있다.


\[X_{k}=\sum_{n=0}^{N-1}e^{-2\pi ikn/N}x_{n}=\sum_{n=0}^{N/2-1}e^{-2\pi ik(2n)/N}x_{2n}+\sum_{n=0}^{N/2-1}e^{-2\pi ik(2n+1)/N}x_{2n+1}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n}+e^{-2\pi ik/N}\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n+1}\]


이제 수열 \(Y_{k}\)와 \(Z_{k}\)를 다음처럼 정의하자.


\[Y_{k}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n}\]

\[Z_{k}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n+1}\]


수열 \(Y_{k}\)와 \(Z_{k}\)를 계산할 수 있다면, 수열 \(Z_{k}\)는 다음과 같은 방법으로 \(O(N)\)의 시간복잡도로 계산할 수 있다.


\[X_{k}=Y_{k}+e^{-2\pi ik/N}Z_{k}\]

\[X_{k+N/2}=Y_{k}-e^{-2\pi ik/N}Z_{k}\]


그런데, 수열 \(Y_{k}\)와 \(Z_{k}\)는 수열 \(x_{2n}\)과 \(x_{2n+1}\)에 대한 이산 푸리에 변환이므로, 분할정복을 통해 총 \(O(N\log N)\)의 시간복잡도로 이산 푸리에 변환을 구할 수 있다.


#include <cstdio>
#include <cmath>
#include <complex>

const int SZ = 20, N = 1 << SZ;
const double PI = acos(-1);

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

void FFT(complex<double> *a, bool f) {
	complex<double> x, y, z;
	double w;
	int i, j, k;
	for (i = 0; i < N; i++) {
		j = Rev(i);
		if (i < j) {
			z = a[i];
			a[i] = a[j];
			a[j] = z;
		}
	}
	for (i = 1; i < N; i <<= 1) {
		w = PI / i;
		if (f) w = -w;
		x = complex<double>(cos(w), sin(w));
		for (j = 0; j < N; j += i << 1) {
			y = complex<double>(1);
			for (k = 0; k < i; k++) {
				z = a[i + j + k] * y;
				a[i + j + k] = a[j + k] - z;
				a[j + k] += z;
				y *= x;
			}
		}
	}
	if (f) for (i = 0; i < N; i++) a[i] /= N;
}

complex<double> X[N];

int main() {
	double x;
	int i, n;
	scanf("%d", &n);
	for (i = 0; i <= n; i++) {
		scanf("%lf", &x);
		X[i] = complex<double>(x, 0);
	}
	FFT(X, false);
	for (i = 0; i < N; i++) X[i] *= X[i];
	FFT(X, true);
	for (i = 0; i <= n + n; i++) printf("%.0f ", X[i].real());
}


~


일반화된 이산 푸리에 변환


지금까지는 함수 \(\mathbb{Z}_{N}\rightarrow\mathbb{C}\)에 대한 이산 푸리에 변환에 대해서만 살펴보았다.


한편 이산 푸리에 변환에 필요한 연산은 덧셈과 복소수곱셈뿐이므로 이 둘이 정의된 모든 집합에 대해서 이산 푸리에 변환이 가능하다.


예를 들어, 함수 \(\mathbb{Z}_{M}\rightarrow\mathbb{C}\)는 덧셈과 복소수곱셈이 가능하므로 \(\mathbb{Z}_{N}\rightarrow\mathbb{Z}_{M}\rightarrow\mathbb{C}\)에 대한 이산 푸리에 변환이 가능하다.


이를 통해 두 \(N\times M\) 행렬 \(A_{i,j}\)와 \(B_{i,j}\)에 대하여 다음을 만족하는 행렬 \(C_{i,j}\)를 구할 수 있다.


\[C_{i,j}=\sum_{k=0}^{N-1}\sum_{l=0}^{M-1}A_{k,l}B_{i-k,j-l}\]


template을 사용하여 구현하면 다음과 같다.


#include <cstdio>
#include <cmath>
#include <complex>

const int SZ = 10, N = 1 << SZ;
const double PI = acos(-1);

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

template <typename T>
struct Func {
	T a[N];

	T &operator [](int x) {
		return a[x];
	}

	Func<T> operator +(Func<T> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] + x[i];
		return r;
	}

	Func<T> operator -(Func<T> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] - x[i];
		return r;
	}

	Func<T> operator *(complex<double> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] * x;
		return r;
	}

	Func<T> operator *=(complex<double> &x) {
		int i;
		for (i = 0; i < N; i++) a[i] *= x;
		return *this;
	}

	Func<T> operator *=(Func<T> &x) {
		int i;
		for (i = 0; i < N; i++) a[i] *= x[i];
		return *this;
	}

	void FFT(bool f) {
		complex<double> x, y;
		T z;
		double w;
		int i, j, k;
		for (i = 0; i < N; i++) {
			j = Rev(i);
			if (i < j) {
				z = a[i];
				a[i] = a[j];
				a[j] = z;
			}
		}
		for (i = 1; i < N; i <<= 1) {
			w = PI / i;
			if (f) w = -w;
			x = complex<double>(cos(w), sin(w));
			for (j = 0; j < N; j += i << 1) {
				y = complex<double>(1);
				for (k = 0; k < i; k++) {
					z = a[i + j + k] * y;
					a[i + j + k] = a[j + k] - z;
					a[j + k] = a[j + k] + z;
					y *= x;
				}
			}
		}
		if (f) {
			x = complex<double>((double)1 / N);
			for (i = 0; i < N; i++) a[i] *= x;
		}
	}
};

Func<Func<complex<double> > > X;

int main() {
	double x;
	int i, j, n, m;
	scanf("%d%d", &n, &m);
	for (i = 0; i <= n; i++) for (j = 0; j <= m; j++) {
		scanf("%lf", &x);
		X[i][j] = complex<double>(x, 0);
	}
	for (j = 0; j < N; j++) X[j].FFT(false);
	X.FFT(false);
	X *= X;
	X.FFT(true);
	for (j = 0; j < N; j++) X[j].FFT(true);
	for (i = 0; i <= n + n; i++) {
		for (j = 0; j <= m + m; j++) printf("%.0f ", X[i][j].real());
		printf("\n");
	}
}


~


XOR Convolution


\(2^{M}\) 크기의 배열 \(A_{i}\)와 \(B_{i}\)에 대하여 다음처럼 정의되는 배열 \(C_{i}\)를 구하는 문제를 생각해 보자. (\(\oplus\)는 Bitwise XOR 연산을 뜻함)


\[C_{i}=\sum_{k=0}^{2^{M}-1}A_{k}B_{i\oplus k}\]


XOR 연산은 각 자리별로 더한 뒤 2로 나눈 나머지를 구하는 것과 같으므로, \(\mathbb{Z}_{2^{M}}\rightarrow\mathbb{C}\)를 다음처럼 펼쳐서 생각해 보자.


\[\mathbb{Z}_{2}\rightarrow\mathbb{Z}_{2}\rightarrow\cdots\rightarrow\mathbb{Z}_{2}\rightarrow\mathbb{C}\]


각각이 이산 푸리에 변환 가능하므로, \(\mathbb{Z}_{2}\)에 대한 이산 푸리에 변환을 \(M\)번 행하는 것임을 알 수 있다.


#include <cstdio>
#include <complex>

const int SZ = 20, N = 1 << SZ;

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

void FFT(int *a, bool f) {
	int i, j, k, z;
	for (i = 0; i < N; i++) {
		j = Rev(i);
		if (i < j) {
			z = a[i];
			a[i] = a[j];
			a[j] = z;
		}
	}
	for (i = 1; i < N; i <<= 1) for (j = 0; j < N; j += i << 1) for (k = 0; k < i; k++) {
		z = a[i + j + k];
		a[i + j + k] = a[j + k] - z;
		a[j + k] += z;
	}
	if (f) for (i = 0; i < N; i++) a[i] /= N;
}

int X[N];

int main() {
	int i, n;
	scanf("%d", &n);
	for (i = 0; i < 1 << n; i++) scanf("%d", &X[i]);
	FFT(X, false);
	for (i = 0; i < N; i++) X[i] *= X[i];
	FFT(X, true);
	for (i = 0; i < 1 << n; i++) printf("%d ", X[i]);
}



'수학' 카테고리의 다른 글

Dividing Polynomials  (0) 2017.01.12
きたまさ法  (0) 2016.11.24
Number Theoretic Fast Fourier Transform  (1) 2016.11.02

Generate Connected Graph

DM2016. 11. 7. 02:03
#include "testlib.h"
#include <vector>
#include <unordered_set>

std::vector<int> P;

int Root(int X) {
	return X == P[X] ? X : P[X] = Root(P[X]);
}

void GenConnectedGraph(int N, int M, std::vector<std::pair<int, int> > &R) {

	if (N < 1 || M + 1 < N || M > (long long)N * (N - 1) / 2) return;

	P.resize(N);
	for (int i = 0; i < N; i++) P[i] = i;

	std::unordered_set<long long> S;

	R.resize(M);
	for (int i = 0; i < N - 1; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (Root(u) == Root(v));
		P[Root(u)] = Root(v);
		R[i].first = u;
		R[i].second = v;
		S.insert((long long)u << 32 | v);
	}

	for (int i = N - 1; i < M; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (u == v || S.find((long long)u << 32 | v) != S.end() || S.find((long long)v << 32 | u) != S.end());
		R[i].first = u;
		R[i].second = v;
		S.insert((long long)u << 32 | v);
	}

	shuffle(R.begin(), R.end());
}

int main(int argc, char *argv[]) {
	registerGen(argc, argv, 1);

	int N = 7;
	int M = 10;

	std::vector<std::pair<int, int> > E;
	GenConnectedGraph(N, M, E);

	printf("%d %d\n", N, M);
	for (int i = 0; i < M; i++) printf("%d %d\n", E[i].first, E[i].second);

	return 0;
}

'DM' 카테고리의 다른 글

Generate Tree  (0) 2016.11.07
Testlib  (0) 2016.11.07

Generate Tree

DM2016. 11. 7. 01:35
#include "testlib.h"
#include <vector>

std::vector<int> P;

int Root(int X) {
	return X == P[X] ? X : P[X] = Root(P[X]);
}

void GenTree(int N, std::vector<std::pair<int, int> > &M) {

	P.resize(N);
	for (int i = 0; i < N; i++) P[i] = i;

	M.resize(N - 1);
	for (int i = 0; i < N - 1; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (Root(u) == Root(v));
		P[Root(u)] = Root(v);
		M[i].first = u;
		M[i].second = v;
	}
}

int main(int argc, char *argv[]) {
	registerGen(argc, argv, 1);

	int N = 7;

	std::vector<std::pair<int, int> > E;
	GenTree(N, E);

	printf("%d\n", N);
	for (int i = 0; i < N - 1; i++) printf("%d %d\n", E[i].first, E[i].second);

	return 0;
}

'DM' 카테고리의 다른 글

Generate Connected Graph  (0) 2016.11.07
Testlib  (0) 2016.11.07