重新複習藍書(共 \(323\) 題),爭取 \(1.5\) 個月內完成
此次記錄,過往的題也會重新編寫題解並收錄
補題連結:Here
0x00 基本演算法
位運算
A題:a^b
https://ac.nowcoder.com/acm/contest/996/A
題目描述
求 a 的 b 次方對 p 取模的值,其中 0 <= a,b,p <= 10^9
輸入描述:
三個用空格隔開的整數a,b和p。
輸出描述:
一個整數,表示a^b mod p的值。
例項:
輸入: 2 3 9
輸出: 8
思路:
這道題是要先算出a的b次冪再對其結果進行求模(取餘),因為b最大可為1e+9,按普通做法來做時間複雜度就太大了,顯然這樣過不了題,
能快速算a的b次冪,就能減小時間複雜度,快速冪就是一種不錯的方法。
什麼是快速冪:
快速冪是一種簡化運算底數的n次冪的演算法,理論上其時間複雜度為 O(log₂N),而一般的樸素演算法則需要O(N)的時間複雜度。簡單來說快速冪其實就是抽取了指數中的2的n次冪,將其轉換為時間複雜度為O(1)的二進位制移位運算,所以相應地,時間複雜度降低為O(log₂N)。
程式碼原理:
以 \(a^{13}\) 為例,
先把指數13化為二進位制就是1101,把二進位制數字1101直觀地表現為十進位制則是如下的等式:
這樣一來 \(a^{13}\) 可以如下算出:
完整AC程式碼如下:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;//將long long型別取個別名:ll型別,為了方便
int power(int a, int b, int mod) {
ll ans = 1 % mod;
for (; b; b >>= 1) {
if (b & 1) ans = ans * a % mod;
a = (ll)a * a % mod;//顯式轉化為ll型別進行高精度計算,再隱式轉化為int
}
return ans;
}
int main() {
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0);
int a, b, mod;
cin >> a >> b >> mod;
cout << power(a, b, mod) << endl;
}
B題:Raising Modulo Numbers
與上面A題寫法一樣
typedef long long ll;
int _;
// 稍微優化下上方程式碼:update 21/01/28
ll qpow(ll a, ll b, ll mod) {
ll ans = 1;
a %= mod;
for (; b; a = a * a % mod, b >>= 1)
if (b & 1) ans = ans * a % mod;
return ans;
}
int main() {
// freopen("in.txt", "r", stdin);
ios_base::sync_with_stdio(false), cin.tie(0);
ll M, N;
for (cin >> _; _--;) {
cin >> M >> N;
ll a, b, ans = 0;
while (N--) {
cin >> a >> b;
ans = (ans + qpow(a, b, M)) % M;
}
cout << ans << endl;
}
}
C題:64位整數乘法
連結:https://ac.nowcoder.com/acm/contest/996/C
思路:
類似快速冪的思想,把整數b用二進位制表示,即
typedef long long ll;
int main() {
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0);
ll a, b, p; cin >> a >> b >> p;
ll ans = 0;
for (; b; b >>= 1) {
if (b & 1)ans = (ans + a) % p;
a = (a << 1) % p;
}
cout << ans << endl;
}
⭐D題:最短Hamilton路徑
連結:https://ac.nowcoder.com/acm/contest/996/D
解題思路
AC程式碼:
#define ms(a,b) memset(a,b,sizeof a)
int e[21][21], b[1 << 21][21], n;
int main() {
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0);
cin >> n;
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
cin >> e[i][j];
ms(b, 0x3f); b[1][0] = 0;
for (int i = 0; i < 1 << n; ++i)
for (int j = 0; j < n; ++j) if (i >> j & 1)
for (int k = 0; k < n; ++k) if (~(i >> k) & 1)//if ((i ^ 1 << j) >> k & 1)
b[i + (1 << k)][k] = min(b[i + (1 << k)][k], b[i][j] + e[j][k]);
cout << b[(1 << n) - 1][n - 1] << endl;
}
⭐例題:[NOI2014]起床困難綜合徵
題意:
貪心從高位到低位列舉,檢驗當前位在初始值為\(0\) 情況下的答案是否可以為\(1\) ,如果不能則檢驗當前位初始值能否為 \(1\),並檢驗當前位在初始值為 \(1\) 情況下的答案是否可以為 \(1\)。
int n, m, x;
string str;
pair<string, int> a[100005];
int work(int bit, int now) { // 用參加的第 bit 位進行n次運算
for (int i = 1; i <= n; ++i) {
int x = a[i].second >> bit & 1;
if (a[i].first == "AND")
now &= x;
else if (a[i].first == "OR")
now |= x;
else
now ^= x;
}
return now;
}
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> str >> x;
a[i] = make_pair(str, x);
}
int val = 0, ans = 0;
for (int bit = 29; bit >= 0; bit--) {
int res0 = work(bit, 0), res1 = work(bit, 1);
if (val += (1 << bit) <= m && res0 < res1)
val += (1 << bit), ans += (res1 << bit);
else
ans += (res0 << bit);
}
cout << ans << "\n";
return 0;
}
遞推與遞迴
遞迴實現指數型列舉
int _, n, m, k, x, y;
vector<int> vec;
void calc(int x) {
if (x == n + 1) {
for (int i = 0; i < vec.size(); ++i) cout << vec[i] << " ";
cout << "\n"; // 注意一下,以後輸出回車用 "\n" 而不是 endl
return;
}
calc(x + 1), vec.push_back(x);
calc(x + 1), vec.pop_back();
}
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
cin >> n;
calc(1);
}
遞迴實現組合型列舉
int n, m;
vector<int> vec;
void calc(int x) {
// 剪枝,如果已經選取了超過m個數,
// 或者即使選上剩下所有數也不夠m個就要提前結束搜尋了 ↓
if (vec.size() > m || vec.size() + (n - x + 1) < m) return;
if (x == n + 1) {
for (int i = 0; i < vec.size(); ++i) cout << vec[i] << " ";
cout << "\n"; // 注意一下,以後輸出回車用 "\n" 而不是 endl
return;
}
calc(x + 1), vec.push_back(x);
calc(x + 1), vec.pop_back();
}
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
calc(1);
}
遞迴實現排列型列舉
int n, m;
int order[20];
bool chosen[20];
void cal(int k) {
if (k == n + 1) {
for(int i = 1;i<=n;++i)
cout << order[i] << " ";
cout << endl; return;
}
for (int i = 1;i <= n; ++i) {
if (chosen[i])continue;
chosen[i] = true;
order[k] = i;
cal(k + 1);
chosen[i] = false;
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0);
cin >> n;cal(1);
}
費解的開關
const int N = 6;//因為後續操作讀取的是字串
char g[N][N];
char backup[N][N];//備份 --- 用於記錄每次列舉第1行的情況
int n;
int dx[5] = {-1,0,1,0,0}, dy[5] = {0,0,0,-1,1};//用於表示當前位置及該位置的上下左右位置的偏移量
//改變當前燈及其上下左右燈的狀況
void turn(int x, int y){
for(int i = 0; i < 5; i ++){
int a = x + dx[i], b = y + dy[i];//用於表示當前位置或該位置的上下左右位置
if(a >= 0 && a < 5 || b >= 0 && b < 5){
g[a][b] ^= 1;//用於'0' 和'1'的相互轉換 -----根據二者的Ascll碼值特點
}
}
}
int main(){
cin >> n;
while(n --){
for(int i = 0; i < 5; i ++) cin >> g[i];//讀取資料
int res = 10;//用於記錄操作的結果
for(int op = 0; op < 32; op ++){//列舉第1行燈的狀態 ---- 也可以採用遞迴實現指數型列舉
int step = 0;//用於記錄當前情況的操作步數
memcpy(backup, g, sizeof g);//備份原陣列資料 ---- 因為每次列舉都是一種全新的情況
//列舉第一行,若燈滅,則點亮
for(int j = 0; j < 5; j ++){
if(!(op >> j & 1)){//也可以是 if(op >> j & 1) ,因為二者情況數量相同
step ++;
turn(0, j);//翻轉當前燈的狀況
}
}
//從第一行向下遞推至倒數第二行
for(int i = 0; i < 4; i ++){
for(int j = 0; j < 5; j ++){
if(g[i][j] == '0'){//當前行當前位置燈滅
step ++;
turn(i + 1, j);//改變當前行的下一行該列燈的狀況,使當前行燈亮
}
}
}
//檢驗最後一行燈是否全亮,若存在暗燈,則此方案不成立
bool dark = false;
for(int j = 0; j < 5; j ++){
if(g[4][j] == '0'){
dark = true;
break;
}
}
if(!dark) res = min(step, res);
memcpy(g, backup, sizeof backup);//還原資料,用於下次方案的操作
}
if(res > 6) res = -1;
cout << res << endl;
}
return 0;
}
// 另一種解
int _, a[6], ans, aa[6];
string s;
void dj(int x, int y) {
aa[x] ^= (1 << y);
if (x != 1) aa[x - 1] ^= (1 << y);
if (x != 5) aa[x + 1] ^= (1 << y);
if (y != 0) aa[x] ^= (1 << (y - 1));
if (y != 4) aa[x] ^= (1 << (y + 1));
}
void pd(int p) {
int k = 0;
memcpy(aa, a, sizeof(a));
for (int i = 0; i < 5; ++i)
if (!((p >> i) & 1)) {
dj(1, i);
if (++k >= ans) return;
}
for (int x = 1; x < 5; ++x)
for (int y = 0; y < 5; ++y)
if (!((aa[x] >> y) & 1)) {
dj(x + 1, y);
if (++k >= ans) return;
}
if (aa[5] == 31) ans = k;
}
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
for (cin >> _; _--;) {
memset(a, 0, sizeof(a));
for (int i = 1; i <= 5; ++i) {
cin >> s; // 字串讀入更便利處理
for (int j = 1; j <= 5; ++j) a[i] = a[i] * 2 + (s[j - 1] - '0');
}
ans = 7;
for (int p = 0; p < (1 << 5); p++) pd(p);
cout << (ans == 7 ? -1 : ans) << "\n";
}
return 0;
}
Strange Towers of Hanoi
#define Fi(i, a, b) for (int i = a; i <= b; ++i)
int d[13], f[13];
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
Fi(i, 1, 12) d[i] = d[i - 1] * 2 + 1;
memset(f, 0x3f, sizeof f), f[1] = 1;
Fi(i, 2, 12) Fi(j, 1, i) f[i] = min(f[i], 2 * f[j] + d[i - j]);
Fi(i, 1, 12) cout << f[i] << "\n";
return 0;
}
⭐Sumdiv (AcWing 97. 約數之和)(數論)(分治)
const int p = 9901;
int pow(int x, int y) {
int ret = 1;
for (; y; y >>= 1) {
if (y & 1) ret = 1ll * ret * x % p;
x = (ll)x * x % p;
}
return ret;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0);
int a, b, ans = 1;
cin >> a >> b;
if (!a) return !puts("0");
for (int i = 2, num; i * i <= a; i++) {
num = 0;
while (a % i == 0) a /= i, num++;
if (num)
ans =
ans * (pow(i, num * b + 1) - 1 + p) % p * pow(i - 1, p - 2) % p;
}
if (a > 1) ans = ans * (pow(a, b + 1) - 1 + p) % p * pow(a - 1, p - 2) % p;
cout << ans << "\n";
return 0;
}
⭐Fractal Streets
題解來源:Click Here
題意:
給你一個原始的分形圖,t組資料,對於每組資料,輸入3個數n,h,o (n為在第n級,h,o為兩個房子的編號),求在第n級情況下,編號為h和o的兩個點之間的距離*10為多少。
其中,第n級分形圖形成規則如下:
- 首先先在右下角和右上角複製一遍n-1情況下的分形圖
- 然後將n-1情況下的分形圖順時針旋轉90度,放到左上角
- 最後將n-1情況下的分形圖逆時針旋轉90度 ,放到左下角
編號是從左上角那個點開始計1,沿著道路計數。
這是著名的通過一定規律無限包含自身的分形圖。為了計算方便,我們將題目中房屋編號從0開始編號,那麼S與D也都減掉1.
大體思路:設calc(n,m)求編號為m的房屋編號在n級城市中的座標位置,那麼距離是:calc(n,s-1) 與 calc(n,d-1)的距離。
從n(n > 1)級城市由四座n-1級城市組成,其中:
1.左上的n-1級城市由城市結構順時針旋轉90度,從編號的順序看,該結構還做水平翻轉,座標轉換至n級時如下圖。
2與3.右上和右下和原始城市結構一樣,座標轉換至n級時如下圖。
市由城市結構逆時針旋轉90度,從編號的順序看,該結構也做了水平翻轉。
旋轉座標的變化可通過公式:
(設len = 2(n-1))當旋轉角度是逆時針90度時,也就是順時針270度時,(x,y)->(y, -x),然後再進行水平翻轉,(y,-x)->(-y,-x)。然後再將圖形平移到n級圖形的左下角,在格子上的座標變化是,水平方向增加len - 1個位置,垂直方向增加2len - 1個位置。因此座標(x,y)按照規則轉移到了(2len-1-y,len-1-x).
注意:n-1級格子裡擁有的房子數量是cnt = 22n /4,即22n-2.
當前編號m在N級格子的哪個方位是:m / cnt.
當前編號m在n-1級格子裡的編號是: m %cnt;
詳細程式碼如下:
using ll = long long;
pair<ll, ll> calc(ll n, ll m) {
if (n == 0) return make_pair(0, 0); //邊界
ll len = 1ll << (n - 1), cnt = 1ll << (2 * n - 2);
pair<ll, ll> zb = calc(n - 1, m % cnt);
ll x = zb.first, y = zb.second;
ll z = m / cnt;
switch (z) {
case 0: return make_pair(y, x); break;
case 1: return make_pair(x, y + len); break;
case 2: return make_pair(x + len, y + len); break;
case 3: return make_pair(2 * len - y - 1, len - x - 1); break;
}
}
int main() {
int t;
cin >> t;
while (t--) {
ll n, s, d;
cin >> n >> s >> d;
pair<ll, ll> zb;
pair<ll, ll> bz;
double ans = 0;
zb = calc(n, s - 1); //記得-1 QWQ
bz = calc(n, d - 1);
ll x, y;
x = (zb.first - bz.first), y = (zb.second - bz.second); //邊長居然是10
ans = sqrt(x * x + y * y) * 10; //喜聞樂見 勾股定理
printf("%.0f\n", ans); //四捨五入
}
return 0;
}
非遞迴實現組合型列舉
#include <iostream>
#include <vector>
using namespace std;
vector<int> chosen;
int n, m;
void dfs(int x);
int main() {
cin >> n >> m;
dfs(1);
}
void dfs(int x) {
if (chosen.size() > m || chosen.size() + (n - x + 1) < m) return;
if (x == n + 1) {
// if(chosen.size() == 0) return;
for (int i = 0; i < chosen.size(); i++) printf("%d ", chosen[i]);
puts("");
return;
}
chosen.push_back(x);
dfs(x + 1);
chosen.pop_back();
dfs(x + 1);
return;
}
字首和與差分
A題:HNOI2003]鐳射炸彈
按照藍書上的教程做即可,注意這道題卡空間用int
而不是 long long
。
int g[5010][5010];
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
int N, R;
cin >> N >> R;
int xx = R, yy = R;
for (int i = 1; i <= N; ++i) {
int x, y, w;
cin >> x >> y >> w, ++x, ++y;
g[x][y] = w, xx = max(xx, x), yy = max(y, yy);
}
for (int i = 1; i <= xx; ++i)
for (int j = 1; j <= yy; ++j)
g[i][j] = g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1] +
g[i][j]; //求字首和
int ans = 0;
for (int i = R; i <= xx; ++i)
for (int j = R; j <= yy; ++j)
//用提前算好的字首和減去其他部分再補上多剪的那部分
ans =
max(ans, g[i][j] - g[i - R][j] - g[i][j - R] + g[i - R][j - R]);
cout << ans << "\n";
return 0;
}
B題:IncDec Sequence
設 a 的差分序列為 b.
則對區間 [l, r] 的數都加 1,就相當於 b[l]++, b[r + 1]--.
操作分為 4 種.
① 2 ≤ l ≤ r ≤ n (區間修改)
② 1 == l ≤ r ≤ n(修改字首)
③ 2 ≤ l ≤ r == n + 1 (修改字尾)
④ 1 == l ≤ r == n + 1 (全修改)
其中操作 ④ 顯然無用.
操作 ① 價效比最高.
於是可得出方案:先用操作 ① ,使得只剩下 正數 或 負數 ,剩下的用操作 ② 或 ③ 來湊.
using ll = long long;
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
int n;
cin >> n;
vector<ll> a(n + 1, 0), b(n + 2);
for (int i = 1; i <= n; ++i) cin >> a[i], b[i] = a[i] - a[i - 1];
ll p = 0, q = 0;
for (int i = 2; i <= n; ++i) { // 2~n的正負數和統計
if (b[i] > 0) p += b[i];
else if (b[i] < 0) q -= b[i];
}
cout << max(p, q) << "\n" << llabs(p - q) + 1 << "\n";
return 0;
}
C題:Tallest Cow
差分陣列,對於給出第一個區間a,b,他們之間的人肯定比他們矮,最少矮1,那麼就在a+1位置-1,b位置加1,計算字首和,a+1以及之後的都被-1了,b及以後的不變。
重複的區間,不重複計算。
另一種思路:先將所有的牛的高度都設為最大值 然後在輸入一組數A B時 將A B之間的牛的高度都減一。
map<pair<int, int>, bool> vis;
int c[10010], d[10010];
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
int n, p, h, m;
cin >> n >> p >> h >> m;
while (m--) {
int a, b;
cin >> a >> b;
if (a > b) swap(a, b);
if (vis[make_pair(a, b)]) continue; // 避免重複計算
vis[{a, b}] = true, d[a + 1]--, d[b]++;
}
for (int i = 1; i <= n; ++i) {
c[i] = c[i - 1] + d[i];
cout << h + c[i] << "\n";
}
return 0;
}
二分
⭐二分A題:Best Cow Fences
二分答案,判定是否存在一個長度不小於L的子段,平均數不小於二分的值。如果把數列中的每個數都減去二分的值,就轉換為判定“是否存在一個長度不小於L的子段,子段和非負”。
先分別考慮兩種情況的解法(1、子段和最大【無長度限制】,2、子段和最大,子段長度不小於L)
<==>求一個子段,使得它的和最大,且子段的長度不小於L。
子段和可以轉換為字首和相減的形式,即設\(sumj\)表示\(Ai 到 Aj\)的和,
則有:\(max{A[j+1]+A[j+2].......A[i] } ( i-j>=L ) \\ = max{ sum[i] - min{ sum[j] }(0<=j<=i-L) }(L<=i<=n)\)
仔細觀察上面的式子可以發現,隨著i的增長,j的取值範圍 0~i-L 每次只會增大1。換言之,每次只會有一個新的取值進入 \(min\{sum_j\}\) 的候選集合,所以我們沒必要每次迴圈列舉j,只需要用一個變數記錄當前的最小值,每次與新的取值 sum[i-L] 取min 就可以了。
double a[100001], b[100001], sum[100001];
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
int n, L;
cin >> n >> L;
for (int i = 1; i <= n; ++i) cin >> a[i];
double eps = 1e-5;
double l = -1e6, r = 1e6;
while (r - l > eps) {
double mid = (l + r) / 2;
for (int i = 1; i <= n; ++i) b[i] = a[i] - mid;
for (int i = 1; i <= n; ++i) sum[i] = sum[i - 1] + b[i];
double ans = -1e10;
double min_val = 1e10;
for (int i = L; i <= n; ++i) {
min_val = min(min_val, sum[i - L]);
ans = max(ans, sum[i] - min_val);
}
if (ans >= 0)
l = mid;
else
r = mid;
}
cout << int(r * 1000) << "\n";
return 0;
}
排序
A題: Cinema
經典離散化例題,把電影的語言與字幕和觀眾懂的語言放進一個陣列,然後離散化。
最後統計快樂人數。
const int N = 200006;
int n, m, a[N], x[N], y[N], cinema[N * 3], tot = 0, k, ans[N * 3];
int find(int f) { return lower_bound(cinema + 1, cinema + k + 1, f) - cinema; }
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i], cinema[++tot] = a[i];
cin >> m;
for (int i = 1; i <= m; ++i) cin >> x[i], cinema[++tot] = x[i];
for (int i = 1; i <= m; ++i) cin >> y[i], cinema[++tot] = y[i];
sort(cinema + 1, cinema + tot + 1);
k = unique(cinema + 1, cinema + tot + 1) - (cinema + 1);
memset(ans, 0, sizeof(ans));
for (int i = 1; i <= n; i++) ans[find(a[i])]++;
int ans0 = 1, ans1 = 0, ans2 = 0;
for (int i = 1; i <= m; i++) {
int ansx = ans[find(x[i])], ansy = ans[find(y[i])];
if (ansx > ans1 || (ansx == ans1 && ansy > ans2)) {
ans0 = i;
ans1 = ansx;
ans2 = ansy;
}
}
cout << ans0 << endl;
return 0;
}
當然不用離散化也可以做。
簡單使用 unordered_map
對映個數即可
const int N = 2e5 + 10;
int _, n, x, y, tmp, a[N];
unordered_map<int, int> mp;
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
for (cin >> _; _--;) cin >> tmp, mp[tmp]++;
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) {
int t;
cin >> t;
if (mp[a[i]] > x)
tmp = i, x = mp[a[i]], y = mp[t];
else if (mp[a[i]] == x && mp[t] > y)
tmp = i, y = mp[t];
}
cout << tmp << "\n";
return 0;
}
B題:貨倉選址
排序一下,利用中位數的性質
int n, ans, sum;
int main() {
ios_base::sync_with_stdio(false), cin.tie(0);
cin >> n;
vector<int> v(n);
for (auto &t : v) cin >> t;
sort(v.begin(), v.end());
ans = v[n / 2];
for (auto &t : v) sum += abs(t - ans);
cout << sum << "\n";
return 0;
}
C題:⭐七夕祭
這裡借下 洛凌璃dalao的blog題解
題解
這個題其實是兩個問題:
1.讓攤點上下移動,使得每行的攤點一樣多
2.左右移動攤點,使得每列的攤點一樣多
兩個問題是等價的,就討論第一個
r[i]表示每行的攤點數
然後使得r[i]的一些攤點移動到r[i - 1]和r[i + 1], 類似於"均攤紙牌"
均攤紙牌
有M個人排一排,每個人分別有C[1]~C[M]張拍,每一步中,一個人可以將自己的一張手牌給相鄰的人,求至少需要幾步
顯然, 紙牌總數T能被M整除有解,在有解的情況下, 考慮第一個人:
1.C[1] >= T/M, 第一個人要給第二個人C[1] - T/M張牌
2.C[1] < T/M, 第二個給第一個人T/M - C[1]張牌
本質就是使得第一人滿足要求要|T/M - C[1]|步
那麼滿足第二人就要 |T/M - (C[2] - (T/M - C[1]))| = |2 * T/M - (C[1] + C[2])|步
滿足第三人 |T/M - (C[3] - (T/M - (C[2] - (T/M - C[1]))))| = |3 * T/M - (C[1] + C[2] + C[3])|
到這裡就可以發現,有一段是字首和, 但再仔細化簡以下可以發現
|3 * T/M - (C[1] + C[2] + C[3])|
=|(T/M - C[1]) + (T/M - C[2]) + (T/M - C[3])|
=|(C[1] - T/M) + (C[2] - T/M) + (C[3] - T/M)|
我們可以讓A[i] = C[i] - T/M, S[i]為A[i]的字首和,
那麼對於"均攤紙牌"這道題的答案就是
∑ni=1∑i=1n|S[i]|
對於本題來說,無非是變成了環形問題
直接無腦dp就可以
我們隨便選取一個人k最為斷環的最後一名(即第一個人變為為k + 1),
則從這個人開始的持有的牌數(這行的攤點數), 字首和為
A[k + 1] S[k + 1] - S[k]
A[k + 2] S[k + 2] - S[k]
...
A[M] S[M] - S[k]
A[1] S[M] - S[k] + S[1]
A[2] S[M] - S[k] + S[2]
...
A[k] S[M] - S[k] + S[k]
我們發現S[M] = 0, 所以答案為
|S[k + 1] - S[k]| + ... + |S[M] - S[k]| + |s[M] - S[k] + S[1]| + ... + |S[M] - S[K] + S[k]|
=|S[k + 1] - S[k]| + ... + |S[M] - S[k]| + |-S[k] + S[1]| + ... + |-S[k] + S[k]|
=∑ni=1∑i=1n |S[i] - S[k]|
答案已經很明顯了,像不像"倉貨選址"?
倉貨選址
一條軸上有N家店,每家店的座標為D[1]~D[N],選擇一家點為倉庫向其他商店發貨,求選哪家店,運送距離最短
不就是∑ni=1∑i=1n |D[i] - D[k]| 為答案嗎?
當然是選中位數了啦,
設k左邊有P家店,右邊有Q家店
如果P<Q,那必然將k右移, ans - Q + P,答案明顯變小了
Q>P,同理,故選擇中位數
所以本題的答案就已經近在眼前了, 字首和,求中位數
using ll = long long;
const int maxn = 1e5 + 5;
int n, m, k;
int c[maxn], r[maxn], s[maxn];
ll work(int a[], int n) {
for (int i = 1; i <= n; ++i) s[i] = s[i - 1] + a[i] - k / n;
sort(s + 1, s + 1 + n);
ll ans = 0;
for (int i = 1; i <= n; ++i) ans += abs(s[i] - s[(n >> 1) + 1]);
return ans;
}
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= k; ++i) {
int a, b;
cin >> a >> b, ++c[b], ++r[a];
}
if (k % n + k % m == 0)
cout << "both " << work(c, m) + work(r, n);
else if (k % n == 0)
cout << "row " << work(r, n);
else if (k % m == 0)
cout << "column " << work(c, m);
else
cout << "impossible";
return 0;
}