河南萌新聯賽2024第(一)場

Proaes發表於2024-07-19

先給出比賽連結:
https://ac.nowcoder.com/acm/contest/86639

A 造數

題目問多少次操作可以把0轉為n

操作共有三種

  1. \(+1\)
  2. \(+2\)
  3. \(\times 2\)

能夠發現操作的數字最大是2,那麼這題就可以考慮二進位制。三種操作就能這麼理解:

  1. \(末位+1\)
  2. \(倒數第二位+1\)
  3. \(左移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\)

\[我們可以發現位權為2^{n}的位,以2^{n + 1}為迴圈 \]

那麼我們就能快速的算出總共有幾個迴圈,就能知道迴圈部分有多少個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此題

\[注意x \in [1 , \lfloor n / i \rfloor] \]

這其實是一個比較常見的調和級數最佳化

\( \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} } \)

\[H_{x} \leq G_{x} \leq A_{x} \leq Q_{x} \]

已知連通塊之和為定值,那麼兩個連通塊大小越接近則,這兩個連通塊的乘積越大

即兩個聯通塊相等或者相差僅為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題的題解)

相關文章