BOI 2016
CEOI 2017
BOI 2018
BOI 2019
BOI 2020
CEOI 2020
Genetyka
Dane jest słów o długości nad alfabetem 4-literowym.
Należy znaleźć słowo (będzie dokładnie jedno takie), które różni się od każdego innego
na dokładnie pozycjach.
Bardzo łatwo jest napisać rozwiązanie działające w czasie ,
wystarczy bowiem sprawdzić dla każdej pary słów na ilu pozycjach się różnią.
Mnożenie macierzy
Zrobimy to jednak szybciej.
Główny pomysł to wyrażenie zadania w terminach macierzowych i skorzystanie
z pomysłowego triku.
Na początek skupimy się na podzadaniu 3, w którym alfabet jest ograniczony do dwóch liter.
Niech oznacza wektor długości , który koduje -te słowo w ten sposób, że litery
zamieniamy na liczby oraz .
Przykładowo słowo ACCAC zostanie zakodowane przez .
Rozważmy dwa wektory oraz i obliczmy ich iloczyn skalarny:
Na każdej pozycji, która ma takie same litery, dostaniemy ,
a na pozycjach z różnymi literami dostaniemy .
Zatem wynik iloczynu to będzie liczba pozycji o takich samych literach
minus liczba pozycji o różnych literach.
Jeśli więc chcemy, żeby -te słowo różniło się od -tego na pozycjach, to musi być
Niech macierz będzie zawierać wszystkie słowa ułożone w wierszach
(będzie zatem miała wierszy i kolumn).
Przemnożenie macierzy przez pionowy wektor o długości daje w wyniku
pionowy wektor długości , zawierający na -tej pozycji iloczyn skalarny .
Słowo jest tym, którego szukamy, jeśli przemnożenie macierzy przez pionowy wektor
da nam w wyniku pionowy wektor , w którym wszystkie wartości są równe , oprócz -tej wartości,
która jest oczywiście równa (bo oznacza ).
Niestety, mnożenie macierzy przez wektor umiemy wykonać w czasie ,
a potrzebujemy takich mnożeń (dla każdego wyboru ), zatem nadal
daje to nam złożoność czasową .
Sprytna sztuczka -- sprawdzanie wyniku mnożenia macierzy
Okazuje się jednak, że sprawdzenie, czy wynik mnożenia macierzy jest konkretną macierzą
można zrobić szybciej niż obliczenie wyniku mnożenia od zera.
Chcemy sprawdzić, czy daje w wyniku .
To sprowadza się do sprawdzenia układu równań (dla ):
Zrobimy to metodą probabilistyczną.
Losujemy dużych liczb i sumujemy wszystkie równania
(modulo duża liczba pierwsza ), przemnażając -te z nich przez liczbę :
Jeśli układ był prawidłowy, to powyższa suma da 0.
Okazuje się jednak, że jeśli suma ta jest 0, to z dużym prawdopodobieństwem układ równań musi być prawidłowy.
Wynika to z lematu Schwartza--Zippela,
który orzeka, że niezerowy wielomian zmiennych o stopniu nad ciałem ewaluuje się do 0
dla losowych wartości z prawdopodobieństwem .
Wykonywanie obliczeń modulo liczba pierwsza oznacza działania nad ciałem rozmiaru .
Jeśli zatem weźmiemy wielomian
to dla losowych jest bardzo mała szansa ,
że będzie on 0, chyba że wielomian jest zerowy, czyli
wszystkie są zerami.
Biorąc szansa pomyłki jest bardzo mała.
W razie potrzeby możemy ją zwiększyć, przez wzięcie większej liczby lub wykonanie obliczenia niezależnie
dla kilku różnych wartości .
Zatem, wystarczy sprawdzić, czy
Po prawej stronie mamy po prostu
a po lewej stronie mamy:
gdzie wartość jest po prostu sumą elementów -tej kolumny macierzy , przemnożonych przez
kolejne liczby .
Wyznaczyć wektory możemy w czasie .
Dzięki temu każde sprawdzenie wektora będzie działać w czasie .
Ostatecznie, złożoność czasowa całego algorytmu wyniesie .
const int N = 4110, P = 1000000007;
int n,m,K;
char w[N][N];
int a[N][N];
int p[N],sum_p;
int b[N];
int main() {
scanf("%d%d%d",&n,&m,&K);
REP(i,n) scanf("%s",w[i]);
REP(i,n) REP(j,m) {
a[i][j] = w[i][j] == 'A' ? 1 : P-1;
}
REP(i,n) {
p[i] = rand() % P;
sum_p = (sum_p + p[i]) % P;
}
sum_p = ll(sum_p) * (P + m - 2*K) % P;
REP(i,n) REP(j,m) {
b[j] = (b[j] + ll(a[i][j]) * p[i]) % P;
}
int ans = -1;
REP(s,n) {
int sum = 0;
REP(i,m) {
sum = (sum + ll(b[i]) * a[s][i]) % P;
}
if (sum == (sum_p + ll(p[s]) * (2*K)) % P) {
ans = s+1;
}
}
printf("%d\n", ans);
}
Dowolny alfabet
Zrobimy sztuczkę, dzięki której dla dowolnego alfabetu rozmiaru , algorytm będzie działał
w czasie .
Każde słowo zamieniamy na słowo binarne o długości , przez zastąpienie
-tej litery alfabetu -literowym słowem składającym się z liter A
i jednej litery C na -tej pozycji.
Zatem dla litery zastępujemy przez ciągi CAAA, ACAA, AACA i AAAC.
Ciągi odpowiadające różnym literom różnią się na dokładnie dwóch pozycjach.
Zatem dobre słowo różni się od wszystkich pozostałych na dokładnie pozycjach.
Wystarczy zatem wprowadzić następującą poprawkę do poprzedniego algorytmu:
code['A'] = 0;
code['C'] = 1;
code['G'] = 2;
code['T'] = 3;
REP(i,n) REP(j,4*m) {
a[i][j] = P-1;
}
REP(i,n) REP(j,m) {
a[i][4*j + code[w[i][j]]] = 1;
}
m *= 4;
K *= 2;