先給出比賽連結:
https://ac.nowcoder.com/acm/contest/86639
A 造數
題目問多少次操作可以把0轉為n
操作共有三種
- \(+1\)
- \(+2\)
- \(\times 2\)
能夠發現操作的數字最大是2,那麼這題就可以考慮二進位制。三種操作就能這麼理解:
- \(末位+1\)
- \(倒數第二位+1\)
- \(左移1位\)
那麼我們就能把n轉成2進位制來求值
以n = 5為例
\(n = 5 = (101)_{2}\)
\( 0 \to 10 \to 100 \to 101 \)
可以發現,噹噹前位置為0時只需要1次操作就能填好這一位,噹噹前位置為1時則需要2次操作來填好這一位。
所以我們只需要把n轉成二進位制01串,然後遍歷這個01串(注意不用遍歷最高位,因為大於2時最優策略肯定是剛開始先+2)答案加上當前位再加1就行。(注意n為1時需要特判答案為1,為0時則不需要,因為不會進迴圈)
Show Code A
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
auto to2 = [&](ll n) {
string res = "";
while (n) {
res += n % 2 + '0';
n /= 2;
}
return res;
};
ll n = 0;
cin >> n;
if (n == 1) {
cout << 1 << "\n";
} else {
int ans = 0;
string s = to2(n);
int len = s.size();
for (int i = 0; i < len - 1; ++ i) {
ans += 1 + (s[i] - '0');
}
cout << ans << "\n";
}
}
D 小藍的二進位制詢問
題目要求區間 [l,r] 中所有整數在二進位制下1的個數之和並998244353取模。
對於一個1e18內的數,我們能log級別求出這個數各位上的1的個數
那能否快速求出這個數以內的各位上的1的個數呢?這樣我們就能透過類似字首和的操作來求出區間內的所有的1的個數了。
事實上是可以的
下面是0~16各個數以及它的二進位制:(2進位制左邊為低位)
$ 10進位制 $ | $ 2進位制 $ |
---|---|
\(0\) | \(000000\) |
\(1\) | \(100000\) |
\(2\) | \(010000\) |
\(3\) | \(110000\) |
\(4\) | \(001000\) |
\(5\) | \(101000\) |
\(6\) | \(011000\) |
\(7\) | \(111000\) |
\(8\) | \(000100\) |
\(9\) | \(100100\) |
\(10\) | \(010100\) |
\(11\) | \(110100\) |
\(12\) | \(001100\) |
\(13\) | \(101100\) |
\(14\) | \(011100\) |
\(15\) | \(111100\) |
\(16\) | \(000010\) |
那麼我們就能快速的算出總共有幾個迴圈,就能知道迴圈部分有多少個1了;再加上非迴圈部分就能知道1cur這一位上有多少個1了對每一位求和就能知道1cur各位上共有幾個1了
但直接這麼算由於資料太大很可能會爆ll(即超過ll能表示的數字上限),我們就可以對l - 1 , r分別進行拆位字首和每一位都用1 ~ r當前位1的個數減去1 ~ l - 1當前位1的個數 再取模就不會爆ll了
Show Code D
constexpr ll mod = 998244353;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
vector p(63);
p[0] = 1ll;
for (int i = 1; i <= 62; ++ i) {
p[i] = p[i - 1] * 2ll;
}
auto bitprefix = [&](ll n) { // 拆位字首和
vector res(64);
for (int i = 0; i <= 61; ++ i) {
if (n / p[i] % 2ll == 1ll) { // 當前位需要考慮非迴圈部分
res[i] = (n + 1ll) / p[i + 1] * p[i + 1] / 2ll + (n + 1ll) % p[i];// 計算迴圈部分與非迴圈部分
} else { // 當前位不需要考慮非迴圈部分
res[i] = (n + 1ll) / p[i + 1] * p[i + 1] / 2ll; // 計算迴圈部分
}
}
return res;
};
auto query = [&](ll l , ll r) {
ll res = 0;
vector sl = bitprefix(l - 1ll);
vector sr = bitprefix(r);
for (int i = 0; i <= 61; ++ i) {
res += (sr[i] - sl[i]) % mod;
res %= mod;
}
return res;
};
int tt = 1;
cin >> tt;
while (tt--) {
ll l , r;
cin >> l >> r;
cout << query(l , r) << "\n";
}
}
F 兩難抉擇新編
這題與H類似,但是x的上界縮小了,並且求的不是陣列和了,而是陣列異或和,即所有的陣列元素異或和
異或及其性質
異或在C++中的運算子是 ^
異或可以理解為按位不進位加法
1.異或的逆運算就是異或本身 如果 a ^ b = c ,那麼 c ^ b = a
2.異或滿足交換律 即 a ^ b == b ^ a
3.異或滿足結合律 即 (a ^ b) ^ c == a ^ (b ^ c)
4.異或滿足分配律 即 a ^ (b & c) == (a ^ b) & (a ^ c)
對於普通加法可以用高斯定律 sn = (1 + n) * n / 2 快速計算1~n的值
對於異或運算來說也有快速計算1~n各數的異或和的方法,即:
s(n)為1到n的數的異或和
s(n) = 1 , n % 4 == 1
s(n) = 0 , n % 4 == 3
s(n) = n , n % 4 == 0
s(n) = n + 1 , n % 4 == 2
程式碼實現如下:
auto xorprefix = [&](ll n) {
int flag = n % 4;
if (flag == 0) {
return n;
} else if (flag == 1) {
return 1;
} else if (flag == 2) {
return n + 1;
} else if (flag == 3) {
return 0;
}
};
根據異或的性質,我們並不能直接找到一個操作使得使得陣列異或和最大
但是我們可以寫出樸素的做法,即對每個數加或者乘可能的x的值算出此時的陣列異或和。透過上面提到的異或的性質我們可以知道,當陣列異或和為sumxor時,只需要 sumxor ^ a[i] 就能刪掉陣列中a[i]的貢獻,此時再異或上a[i]改變的值就能求出此時的陣列異或和,取最大就行了
然而其實這個樸素做法就能AC此題
這其實是一個比較常見的調和級數最佳化
\( \sum\limits_{i = 1}^{n} \sum\limits_{j = 1}^{\frac{n}{i}} 1 = \sum\limits_{i = 1}^{n} \frac{n}{i} = n \sum\limits_{i = 1}^{n} \frac{1}{i} < n(1 + ln(n)) \)
下面給出證明:
\( \int_{1}^{n} \frac{1}{x} = ln(x) \vert_{1}^{n} = ln(n) \)
透過影像法可知
\( \sum\limits_{i = 2}^{n} (i - (i - 1)) * \frac{1}{i} = \sum\limits_{i = 2}^{n} \frac{1}{i} < \int_{1}^{n} \frac{1}{x} = ln(x) \)
所以
\( \sum\limits_{i = 1}^{n} \frac{1}{i} < 1 + ln(x) \)
這個複雜度是可以容忍的
Show Code F
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
ll n , ans = 0 , sumxor = 0;
cin >> n;
vector a(n + 1);
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
sumxor ^= a[i];
}
for (int i = 1; i <= n; ++ i) {
ll cur = sumxor;
cur ^= a[i];
for (int x = 1; x <= n / i; ++ x) {
ans = max(ans , cur ^ (a[i] + x));
ans = max(ans , cur ^ (a[i] * x));
}
}
cout << ans << "\n";
}
H 兩難抉擇
這題讓我們對陣列進行操作來使得陣列總和最大
共有兩種操作:
\(
1.選擇陣列中的一個數使之加上x,~~~ x \in [1 , n]
\)
\(
2.選擇陣列中的一個數使之乘上x,~~~ x \in [1 , n]
\)
已知陣列中元素是恆正的,那麼要使陣列和最大,且只能操作一次,對於操作1來說,自然是x選擇n最大才能對陣列的貢獻最大(無論對哪個數加x貢獻都一樣);對於操作2來說,x選擇最大的ai乘上n貢獻最大
Show Code H
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
ll n;
cin >> n;
vector a(n);
for (int i = 0; i < n; ++ i) cin >> a[i];
sort(a.begin(),a.end());
a[n - 1] = max(a[n - 1] + n , a[n - 1] * n);
// 求和函式,for迴圈直接求也可以
cout << accumulate(a.begin() , a.end() , 0ll) << "\n";
}
I 除法移位
題目要求最多t次迴圈右移,第幾次操作使得陣列的第一位元素除以其他所有元素的值最大
當第一個元素變大時,後面必有某個元素變小了,那麼此時值一定大於變化前的值,所有我們只需要找到最多t次迴圈右移,元素的第一位何時最大就行。
Show Code I
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n , t;
cin >> n >> t;
vector a(n + 1);
for (int i = 0; i < n; ++ i) cin >> a[i];
a[n] = a[0];
int maxn = 0 , maxi = 0 , cur = 0;
for (int i = n; i >= 0 && cur <= t; -- i , ++ cur) {
// 當有多種答案時,輸出最小值,故不能取等號
if (a[i] > maxn) {
maxn = a[i];
maxi = cur;
}
}
cout << maxi << "\n";
}
K 圖上計數(easy)
你有一張 n 個點 m 條邊的無向圖,你有無數次刪除操作來刪除任意條邊以獲得若干個聯通塊。定義聯通塊的大小為其所包含點個數。定義這個圖的代價是:你有任意次操作,每次操作合併兩個聯通塊,合併後聯通塊大小為二者之和,最後剩下兩個聯通塊大小的乘積為此圖的代價,若只有一個則代價為0。你需要最大化此圖代價。
因為你可以任意刪邊,也可以隨意合併,那麼就可以隨意構造連通塊了。
根據基本不等式鏈
\( H_{n} = \frac{n}{\sum\limits_{i = 1}^{n} \frac{1}{x_{i}}} = \frac{n}{ \frac{1}{x_{1}} + \frac{1}{x_{2}} + \dots + \frac{1}{x_{n}}} \)
\( G_{n} = \sqrt[n]{\prod\limits_{i = 1}^{n} x_{i}} = \sqrt[n]{x_{1} x_{2} \dots x_{n}} \)
\( A_{n} = \frac{1}{n} \sum\limits_{i = 1}^{n} x_{i} = \frac{ x_{1} + x_{2} + \dots + x_{n} }{n} \)
\( Q_{n} = \sqrt{ \frac{1}{n} \sum\limits_{i = 1}^{n} x_{i}^{2} } = \sqrt{ \frac{ x_{1}^{2} + x_{2}^{2} + \dots + x_{n}^{2} }{n} } \)
已知連通塊之和為定值,那麼兩個連通塊大小越接近則,這兩個連通塊的乘積越大
即兩個聯通塊相等或者相差僅為1的時候最大
Show Code K
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
ll n , m;
cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int u , v;
cin >> u >> v;
}
ll ans = ((n) / 2) * ((n + 1) / 2);
cout << ans << "\n";
}
(PS:菜菜,目前只寫了6題的題解)