Strona główna » Algorytmy » Artykuły » Jedno Rozwiązanie?
 

Jedno Rozwiązanie?

Zadanie

Dane jest równanie a = b AND c oraz dwie wartości całkowite a i b. Zakładając, że c jest mniejsze niż 2b to ile jest możliwych rozwiązań? Napisz program, który wypisze je wszystkie w dowolnej kolejności. Jeśli nie jest możliwe wyznaczenie c to program powinien wypisać odpowiedni komunikat.

Przykład

Przykładowo mamy dane a = 4 = 1002 oraz b = 5 = 1012. Wiadomo, że c to liczba należąca do przedziału [0, 10), ale tylko dwie spełniają równanie. Są to 4 oraz 6. Wystarczy sprawdzić, że 100 = 101 AND 100 oraz 100 = 101 AND 110.

Implementacja

Rozwiązanie Siłowe

Rozwiązanie siłowe polega na napisaniu funkcji, która w pętli sprawdzi wszystkie możliwe rozwiązania. Złożoność takiego algorytmu jest liniowa O(n). Warto jednak zauważyć, że rozwiązania stanowią niewielki procent wszystkich możliwych. Oto przykładowy kod funkcji:

  1. static List<int> SzukajRozwiazan(int a, int b)
  2. {
  3. List<int> wynik = new List<int>();
  4. for (int c = 0; c < 2 * b; c++)
  5. {
  6. if (a == (b & c))
  7. {
  8. wynik.Add(c);
  9. }
  10. }
  11. return wynik;
  12. }

Na początku deklarowana jest pusta lista do zapisu danych, a następnie definiowany jest odpowiedni zakres w pętli i jeśli aktualny indeks po wykonaniu operacji binarnej da odpowiedni wynik to wartość indeksu zostaje dopisana na listę. Na koniec zwracana jest lista wynikowa.

Rozwiązanie Optymalne

Opis

Złożoność algorytmu można poprawić wyznaczając miejsca, gdzie trzeba postawić konkretną wartość, a kiedy dowolną. Oto możliwe pary (a, b) oraz akcja jaką należy podjąć:

abc
00dowolna wartość, 0 lub 1
01tylko 0
10niemożliwe, cokolwiek i 0 daje zawsze 0
11tylko 1

Jak można zauważyć jeśli w parze (a, b) wystąpi para wartości (1, 0) to zadanie jest niemożliwe do rozwiązania. Jedynym przypadkiem, gdy można postawić dowolną wartość jest (0, 0), ponieważ cokolwiek i 0 zawsze da 0.

Algorytm można usprawnić następująco: na początku znajdujemy wszystkie bity o dowolnej wartości i zapisujemy ich pozycje na liście [p1, p2, .., pk]. Podczas przeglądania wartości możemy zakończyć działanie funkcji jeśli znajdziemy parę (1, 0). Na podstawie listy określamy, że jest 2k możliwych rozwiązań i je wyznaczamy. n-te rozwiązanie polega na zastąpieniu bitu pi poprzez i-ty bit n.

Przykład

Dane jest a = 1002 oraz b = 1012. Wiadomo, że dowolny może być bit środkowy, a więc o indeksie 1. Istnieję tylko dwa możliwe rozwiązania. Podstawą do generowanie kolejnych rozwiązań jest 1x0. Najpierw x to 0, a potem 1. W ten sposób zostają uzyskane obydwa możliwe rozwiązania.

Kod

Poniższy kod implementuje powyższe wyjaśnienie algorytmu i zwraca listę wszystkich możliwych rozwiązań:

  1. static List<int> SzukajRozwiazan(int a, int b)
  2. {
  3. List<int> wynik = new List<int>();
  4. List<int> dowolne = new List<int>();
  5. int maska = 0;
  6. int dl = (int)Math.Log(Math.Max(a, b), 2) + 1;
  7. for (int i = 0; i < dl; i++)
  8. {
  9. if (((a >> i) & 1) == 0)
  10. {
  11. if (((b >> i) & 1) == 0)
  12. dowolne.Add(i);
  13. }
  14. else
  15. {
  16. if (((b >> i) & 1) == 0)
  17. return wynik;
  18. else
  19. UstawBit(ref maska, i, 1);
  20. }
  21. }
  22. dl = 1 << dowolne.Count;
  23. for (int i = 0; i < dl; i++)
  24. {
  25. for (int j = 0; j < dowolne.Count; j++)
  26. {
  27. UstawBit(ref maska, dowolne[j], (i >> j) & 1);
  28. }
  29. wynik.Add(maska);
  30. }
  31. return wynik;
  32. }

Dla k wierszy inicjujemy licznik na zero oraz kopiujemy aktualny numer wiersza. Nastepnie w petli przechodzimy po kolejnych bitach tak dlugo, az wyzerujemy wartosc. Za kazdym razem dodajemy najstarszy bit liczby. Na koniec obliczamy 2ile i wypisujemy wynik.

Pierwsza część polega na wyznaczeniu maski rozwiązań, która będzie potem wykorzystana do generowania rozwiązań (zmieniane będę później tylko odpowiednie bity). W zależności od wartości i-tego bitu modyfikujemy maskę. Następnie przechodzimy do generowania rozwiązań odpowiednio modyfikując bity maski i zapisując dane do wyniku. Złożoność algorytmu to O(nk), gdzie n - ilość rozwiązań, a k - ilość bitów dowolnych. Zaletą takiego rozwiązania jest to, że w każdej iteracji znajduje rozwiązanie i potrafi wyjść z funkcji poprzez wykrycie niemożliwego stanu.

W kodzie została wykorzystana metoda UstawBit(), która w podanej zmiennej ustawia i-ty bit na wartosc.

  1. static void UstawBit(ref int zmienna, int i, int wartosc)
  2. {
  3. zmienna &= ~(1 << i);
  4. zmienna |= (wartosc << i);
  5. }

Testowanie funkcji

W celu przetestowania kodu można skorzystać z poniższego fragmentu programau:

  1. static void Main(string[] args)
  2. {
  3. Console.Write("Podaj liczbę wynikową\n a = ");
  4. int a = Convert.ToInt32(Console.ReadLine());
  5. Console.Write("Podaj pierwszy składnik\n b = ");
  6. int b = Convert.ToInt32(Console.ReadLine());
  7. List<int> wynik = SzukajRozwiazan(a, b);
  8. if (wynik.Count == 0)
  9. {
  10. Console.WriteLine("Nie ma rozwiązań");
  11. }
  12. else
  13. {
  14. Console.WriteLine("Oto rozwiązania:");
  15. foreach(int x in wynik) {
  16. Console.Write("{0} ", x);
  17. }
  18. }
  19. Console.ReadKey();
  20. }