CodeForces Triple Operations (1999E) 題解
題意
把閉區間\([l, r]\)的數字都寫出來。每步操作中,要選擇兩個數\(a\)、\(b\),使其變為\(3a\)和\(\lfloor \frac{b}{3} \rfloor\)。求出使所有數都等於\(0\),所需的運算元。已證明解總是存在。
思路
發現1
在樣例中可見,當有數字已為\(0\)時,可以直接令\(0\)增大3倍,也就是不改變它,而同時減小其他任意的數。
發現2
觀察每步操作分別對兩個數的改變:當兩數都不為\(0\)時,\(a\)增大\(3\)倍,\(b\)除以\(3\),但之後肯定無法“逃避”將\(a\)增大的\(3\)倍除回\(a\)。又因為對任意\(3^ka(k>0)\)來說,總要進行\(k\)次操作才能使其變回\(a\)。這說明如果我們不用\(0\)來配合減小其他數的話,我們並沒有使操作變少,且要將\(a, b\)變為\(a, 0\),我們至少要操作\(2\)倍\(b\)變為\(0\)需要的次數。
發現3
一個數要除\(3\)除至為\(0\),需要的操作次數為\(\lfloor\log_{3}Num\rfloor + 1\)。又因為指數函式的單調性,所需操作次數越少,數字一定越小。
推論1
當有\(0\)時,只能透過\(0\)來“真正地”減小不為\(0\)的數。當沒有\(0\)時,我們必須要對某數\(k\),執行\(2\times \left (\lfloor\log_{3}k\rfloor + 1 \right )\)次操作,使之變成\(0\)。
結論
答案就是最小數運算元的兩倍,加上其他所有數所需的運算元,即\(ans = 2 \times op(l) + \sum_{i = l+1}^{r}op(i)\),其中\(op(x) = \lfloor\log_{3}x\rfloor + 1\)。
最佳化
但僅僅由上面的結論,直接寫程式碼,恐怕會超時。關鍵在於題目最差的情況下,可以給出\(10^4\)個樣例全是\(\left[1,2\cdot10^5\right]\),這樣就要執行\(2\times10^9\)次,超時。
顯然,最佳化的關鍵在於不能一個個計算,我們需要更快的方法。觀查操作次數的式子其中的\(3\)與下取整符號,不難發現在連續的區間內,只有遇到\(3\)的某次方數時,操作次數才會改變。
那麼我們就只需要先預處理出足夠多的\(3\)的\(n\)次冪,再一段一段地從\(l\)處理到\(r\)即可。
透過計算器不難算出,\(3\)的\(20\)次冪已經可以滿足題目所需。
程式碼
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 在main中預處理出20個3的次方
ll pow3[21];
void solve()
{
ll l, r;
cin >> l >> r;
int pos = 0;
while (pos < 20 && pow3[pos] <= l) ++pos;
// 第一次特殊,因為要先找到最小數的op,並加上,以滿足2倍最小值運算元的要求
ll ans = pos;
while (l <= r) {
// 找到恰好大於的地方,因為恰好大於,所以pos即是前面這些數所需的運算元
while (pos < 20 && pow3[pos] <= l) ++pos;
// 因為恰好大於,所以無法保證該數不超過r
ans += (min(r, pow3[pos] - 1) - l + 1) * (ll)pos;
l = pow3[pos];
}
cout << ans << '\n';
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 預處理到3的20次冪
pow3[0] = 1;
ll tmp = 3;
for (int i = 1; i <= 20; ++i) {
pow3[i] = tmp;
tmp *= 3;
}
int T;
cin >> T;
while (T--) {
solve();
}
return 0;
}