給定一個長爲 \(n\) 的數組 \(a\) ,你能夠對任意 $ i, j ;(i\neq j)$ ,進行操做 \(a_j=a_i+a_j\)。同時,須要知足數組中任意一項值不超過 \(k\),求最多可進行多少次操做。node
*800
greedy
算法
這題,顯然是一個離線貪心。不瞭解的能夠查看個人貪心算法專題、數組
排序以後,把最小值不斷賦值給其餘數直到沒法繼續賦值。併發
對於這種題,毫無疑問貪心算法或者數學法會是更好的選擇。ide
const int maxn = 1e5 + 50; int f[maxn], mn, pos; void solve(){ int n = read(), k = read(); for (int i = 0; i < n; ++ i) f[i] = read(); sort(f, f + n); LL sum(0); for (int i = 1; i < n; ++ i) sum += (k - f[i]) / f[0]; cout << sum << '\n'; }
給定一個長爲 \(n\) 的非負整數序列 \(a\) ,和一個定值 \(T\)。spa
定義 \(f(b)\) 爲:指針
序列 \(b\) 中,知足 \(b_i + b_j = T\) 的點對 \((i, j) \; i < j\) 的總數。code
如今,要求對序列 \(a\) 進行黑白染色,使其分爲兩個子序列 \(c, d\) 。求問使得 \(f(c) + f(d)\) 最小的染色方案。排序
*1100
get
本題具備兩個思路,分別爲 基於數學推演的狀況討論法,基於貪心算法的暴力比較法。
假如在競勝過程中,若是能大體保證貪心算法的正確性時,用貪心算法顯然更好一些。
這裏主要介紹貪心算法,數學推演其實是將小於 \(\dfrac{T}{2}\) 的分爲一組, 大於 \(\dfrac{T}{2}\) 的分爲一組。等於 \(\dfrac{T}{2}\) 的在兩子序列中週期擺動安放。
而貪心算法則是維護兩個子序列的 hashmap
。當要插入一個數字時,比較其在兩個子序列中分別能組成多少組合法點對,貪心選擇數量較少的進行插入,知足要求。
在看到題面的時候,經過注意到 \(b_i + b_j = T\) ,咱們應該快速聯想到 hashmap
解決此類問題。
在看到最小時,應該快速思考使用在線 or 離線貪心算法。
const int maxn = 1e5 + 50; int f[maxn], ans[maxn]; void solve(){ int n = read(), T = read(); for (int i = 0; i < n; ++ i) f[i] = read(); unordered_map<int, int> mp1, mp2; mp1.clear(), mp2.clear(); VI ans(n); for (int i = 0; i < n; ++ i){ if (mp1[T - f[i]] <= mp2[T - f[i]]){ ans[i] = 0; ++ mp1[f[i]]; }else { ans[i] = 1; ++ mp2[f[i]]; } } show(ans); }
給定一個長爲 \(n\) 的序列 \(a\),該序列元素大小均在 \([1, n]\) 中。定義 k驚喜數字:
- 在序列 \(a\) 全部長爲 \(k\) 的連續子序列中均出現過的最小元素,若不存在則賦值爲 \(-1\)。
請你計算並輸出 \(k \in [1, n]\) 的全部 k驚喜數字。
1 <= n <= 3e5
*1500
剛拿到這題時,我沒有一點思路。一直在思考是否是滑動窗口,以及如何複用其中的信息,均發現沒法快速解決。
後面轉換角度從每一個元素的特性出發,插入頭尾兩個虛擬結點,尋找各元素之間的最大間隔。並發現這個方法可行。
最終得出以下結論:
- 假如你不自覺的想入一個問題且在必定時間內不能得出答案時,說明你有一個細節或者角度不對。
- 若是不自覺的用宏觀解決問題時(好比在段,區間),專一於微觀多是這個題目的解決方案(位,各元素之間)
之因此寫上面這段話,是由於這題當時險些沒有作出,並且不屬於方法不清楚的層面,而是我在思考問題時出現了角度的誤差。謹以此警示。
迴歸這個題目,如上文所提,咱們維護每一個元素的最大間隔。易知,當 \(k\) 大於某元素最大間隔時,就可能對k驚喜數字產生貢獻。在利用 std::map
維護每元組的間隔時,因爲其自動排序的特性,能夠從頭到尾進行處理。
const int maxn = 3e5 + 50; int f[maxn]; void solve(){ int n = read(), tot = 0, cnt(0); map<int, int> dis; // 最大間距 unordered_map<int, int> pre; // 上一個元素出現位置 for (int i = 1; i <= n; ++ i){ // index from 1 (in order to insert 0-index node) f[i] = read(); if (pre[f[i]] == 0){ pre[f[i]] = i; dis[f[i]] = i; }else { int dlt = i - pre[f[i]]; // 間距 // wprint(dlt, dis[f[i]]); pre[f[i]] = i; if (dlt > dis[f[i]]) dis[f[i]] = dlt; } } for (auto &e: pre){ int dlt = n + 1 - e.second; // insert (n+1)-index node if (dlt > dis[e.first]) dis[e.first] = dlt; // wprint(dlt, e.first, e.second); } vector<int> ans(n + 1, -1); int stop = n + 1; for (auto &e: dis){ // 利用 map 的排序特性,從頭到尾處理便可 if (e.second >= stop) continue; for (int i = e.second; i < stop; ++ i) ans[i] = e.first; stop = e.second; } for (int i = 1; i <= n; ++ i) cout << ans[i] << (i == n ? '\n' : ' '); }
- 出一個序列 \(a\),求出一個長度不超過 \(3n\) 的操做序列,每次操做以後序列中全部元素必須爲非負整數,操做完成後使序列 \(a\) 中每一個元素相等。
- 定義一次操做爲:選出 \((i,j,x)\) 三元組,知足 \(i,j\) 爲序列合法下標,\(x\) 爲 \(10^9\) 之內非負整數,令 \(a_i:= a_i-x\cdot i,a_j:=a_j+x\cdot i\)。
- 輸出時先輸出操做次數 \(k\),而後輸出 \(k\) 行操做序列。
*2000
*2000
分的構造題,實際上比完賽以後想一想仍是很套路的。之後有機會總結一個 你只能用最多 \(c \cdot n\) 次操做,完成一個目標。
在這裏進行一個簡單的總結。
用 c * n 次操做(詢問)完成目標 首先,無論可用操做(詢問)數爲多少,一個永遠能夠嘗試的思路是: 以某個最特殊的元組作跳板。
- 若 \(c == 1\),則爲線性操做相似冒泡的感受,從頭到尾線性操做,每次保留其中之一在下一次進行操做。
- 若 \(c == 2\),通常來講:
- 可能爲 \(c == 1\)時的狀況,可是每次操做須要兩步C. Chocolate Bunny 相似這題。
- 或者對每一個元素用一次操做將其轉移到跳板上,再進行一次操做完成目的。
- 若 \(c==3\),則想法與上述差很少。
總而言之,須要發現操做的特定性質,如互相操做,跳板操做。
對於這題,比較困難的地方在於每次操做以後保證全部的元素非負。根據上述結論顯然咱們須要選擇一共「跳板
」,毫無疑問這個跳板選擇初始位置 \(1\) 最爲合適。所以,咱們大體制定好了構造策略:
根據題設限制,每一個元素最多轉移 \(a_i - (a_i) \%i\) ,對於餘下的 \(a_i \% i\) ,咱們能夠先補充上 \(i - (a_i \% i)\),再進行轉移。通過思考,這個方案是知足條件的,實際上他也符合一種貪心的思想。咱們儘量避免對於元素進行較大的減操做。
所以最終策略爲:
const int maxn = 1e4 + 50; int f[maxn]; void solve(){ int tot(0), n = read(); for (int i = 1; i <= n; ++ i) f[i] = read(), tot += f[i]; if (tot % n != 0){ cout << "-1\n"; return; } // 不能整除顯然不行 int eve = tot / n; vector< tuple<int, int, int> > res; for (int i = 2; i <= n; ++ i){ if (f[i] % i == 0) res.pb({i, 1, f[i] / i}); else { res.pb({1, i, i - (f[i] % i)}); res.pb({i, 1, (f[i] / i) + 1}); } } for (int i = 2; i <= n; ++ i) res.pb({1, i, eve}); wprint(sz(res)); for (auto &e: res) wprint(get<0>(e), get<1>(e), get<2>(e)); }
給定長度爲 \(n\) \((1\le n\le3\times 10^5)\) 的數列 \(\{a_n\}(0\le a_n\le 10^9)\),請求出最小的整數 \(x\) 使 \(\{a_n\oplus x\}\) 的逆序對數最少,其中$ \oplus$ 是異或。
*2000
divide and conquer
CDQ分治
本題最開始會比較容易想到經過 dp
去解決。考慮到逆序對這個經典的問題,便考慮到分治的思路。實際上 bit
的 0 or 1 能夠類比爲二叉樹的左右孩子。若越靠近根結點表明的 bit
越高,則右孩子必定大於左孩子(用了字典樹的知識)。
所以,題目便比較簡單了:運用 CDQ 分治的思路:
首先,咱們維護一個 dp
數組,dp[bit][i]:= x 的第 bit 位爲 i 時逆序對的個數
。
由於,兩個元素比較大小等價於比較二者第一個不一樣位。不一樣位爲 1
的更大。所以在解題的過程當中,維護兩個數組,左邊表明當前分治到左邊的元素(bit is 0
),右邊表明當前分治到右邊的元素(bit is 1
) 。所以,右邊必定大於左邊,在利用求解逆序對的知識,經過雙指針處理兩個子樹合併出現的逆序對個數。
bit
(等價於 x
的第 bit
位爲 0
),則逆序對爲直接求得的。bit
(等價於 x
的第 bit
位爲 1
),則逆序對數爲總數-不修改時的逆序對個數。以後,貪心選取其中每 bit
逆序對較小的構造答案。
#define pb push_back using VI = vector<int>; using LL = long long; // CDQ 分治處理點對問題 int dp[35][2]; void helper(VI cur, int bit = 30){ if (bit < 0 or cur.empty()) return; int cnt1(0), ans1(0); int cnt2(0), ans2(0); VI right, left; for (auto &x: cur){ if ((x >> bit) & 1){ ans1 += cnt2; // 因爲 index 從大到小,所以 cnt2 表明 index 大於當前元素且元素值小於當前元素的點對數。 ++ cnt1; right.pb(x); }else { ans2 += cnt1; ++ cnt2; left.pb(x); } } helper(left, bit - 1), helper(right, bit - 1); dp[bit][0] += ans1; dp[bit][1] += ans2; } void solve(){ int n = read(); VI a(n); for (auto &&e: a) e = read(); reverse(all(a)); helper(a); LL ans(0), res(0); for (int i = 0; i <= 30; ++ i){ ans += min(dp[i][0], dp[i][1]); if (dp[i][1] < dp[i][0]) res |= (1 << i); } cout << ans << ' ' << res << '\n'; }