Algoritmul, pas cu pas
Drumul e determinist. Pentru un element, operația e impusă de paritate: par , impar . Deci de la există un singur drum către , iar numărul minim de pași e fix lungimea lui — notată . Răspunsul pe un vector e .
Recurența lui . (un impar devine par cu o operație, apoi se înjumătățește cu a doua).
Suma pe interval. Un vector are până la elemente, deci nu pot calcula pentru fiecare. Folosesc funcția de prefix și răspund cu .
Sparg pe pari și impari:
- termenii pari : ;
- termenii impari (cu ): cu , , unde .
Deci:
Ambele apeluri folosesc argumente , deci cu memoizare (un map) numărul de valori distincte e mic. Complexitate: per întrebare.
Soluția în C++
#include <fstream>
#include <map>
using namespace std;
ifstream fin("collatz.in");
ofstream fout("collatz.out");
map<long long, long long> memo;
// P(n) = f(1) + f(2) + ... + f(n)
long long P(long long n) {
if (n <= 1) return 0; // P(0) = P(1) = 0
auto it = memo.find(n);
if (it != memo.end()) return it->second;
long long h = n / 2; // numărul de termeni pari <= n
long long T = (n - 1) / 2; // numărul de termeni impari >= 3 (plus 1)
long long res = h + P(h) + 3 * T + P(T + 1);
memo[n] = res;
return res;
}
int main() {
int T;
fin >> T;
while (T--) {
long long S, D;
fin >> S >> D;
fout << P(D) - P(S - 1) << "\n"; // suma f(x) pe [S, D]
}
return 0;
}- Cauți o secvență „optimă” de operații — nu există alegere: paritatea impune operația, deci e unic determinat; nu e o problemă de optimizare, ci de numărare.
- Aduni element cu element — cu până la e imposibil; folosește funcția de prefix și .
- Uiți memoizarea — fără
maprecurența se ramifică exponențial; cu ea, argumentele distincte sunt polilogaritmice.