2021 高階競程 Contest #8 - 題解
- Task Provider: ys
- Task Setter: ys
首殺 team21_24 (00:04)
解法說明
根據題意:
- 番茄糖可以給真姬、千歌、璃奈
- 柑橘糖可以給千歌、璃奈
- 咖啡糖可以給璃奈
解法 1
明顯的 必須大於等於
因為真姬只吃番茄糖
而當真姬拿完糖果後剩下的 也要大於等於
因為可以給千歌番茄糖和柑橘糖
若最後剩下的 大於等於 則能滿足所有人的期待
因為這三種糖果璃奈都吃
解法 2
可將問題轉換為 flow network,使用現成演算法進行求解
標準程式
解法 1
解法 2
#include<bits/stdc++.h>
using namespace std;
int constexpr MAXN = 8;
int constexpr INF = INT_MAX;
int m, k, r, t, o, c;
struct MaxFlow{
struct edge{ int to, cap, flow, rev; };
vector<edge> v[MAXN];
int s, t, dis[MAXN], cur[MAXN];
int dfs(int u, int cap){
if(u == t || !cap) return cap;
for(int &i = cur[u]; i < v[u].size(); i++){
edge &e = v[u][i];
if(dis[e.to] == dis[u] + 1 && e.flow != e.cap){
int df = dfs(e.to, min(e.cap - e.flow, cap));
if(df){
e.flow += df;
v[e.to][e.rev].flow -= df;
return df;
}
}
}
dis[u] = -1;
return 0;
}
bool bfs(){
memset(dis, -1, sizeof(dis));
queue<int> q;
q.push(s), dis[s] = 0;
while(!q.empty()){
int tmp = q.front();
q.pop();
for(auto &u : v[tmp]){
if(!~dis[u.to] && u.flow != u.cap){
q.push(u.to);
dis[u.to] = dis[tmp] + 1;
}
}
}
return dis[t] != -1;
}
int maxflow(int _s, int _t){
s = _s, t = _t;
int flow = 0, df;
while(bfs()){
memset(cur, 0, sizeof(cur));
while(df = dfs(s, INF)) flow += df;
}
return flow;
}
void addedge(int st, int ed, int cap){
v[st].push_back(edge{ed, cap, 0, v[ed].size()});
v[ed].push_back(edge{st, 0, 0, v[st].size() - 1});
}
};
int main()
{
cin >> m >> k >> r >> t >> o >> c;
MaxFlow fn;
int S = 0, T = 7;
fn.addedge(S, 1, t);
fn.addedge(S, 2, o);
fn.addedge(S, 3, c);
fn.addedge(1, 4, INF);
fn.addedge(1, 5, INF);
fn.addedge(1, 6, INF);
fn.addedge(2, 5, INF);
fn.addedge(2, 6, INF);
fn.addedge(3, 6, INF);
fn.addedge(4, T, m);
fn.addedge(5, T, k);
fn.addedge(6, T, r);
cout << (fn.maxflow(S, T) >= m+k+r? "YES" : "NO") << endl;
return 0;
}
- Task Provider: D4nnyLee
- Task Setter: D4nnyLee
首殺 team21_24 (00:26)
解法說明
令 為門牌號碼為 的住戶的收入。
定義
可以發現陣列 透過 映射之後會有單調性,
因此我們只要在映射之後的陣列上面做二分搜即可。
標準程式
首殺 team21_23 (00:51)
解法說明
- 思考 矩陣的順序是否影響最終結果?否
- 思考「減法」在「同化」之前是否只有好處沒有壞處?是
- 本題枚舉同化人數,從 到 ,找出不同 所需要的操作數量。
- 同化的對象為 矩陣中的較大值,目標為 矩陣中最小值,所以先將 從小到大排序
- IQ =
- 操作數量 = 的減法次數
- 針對每個 計算 所需要的減法次數。
- 補充說明:範例code中的 為除法取頂的實作方案。
標準程式
- Task Provider: leo
- Task Setter: leo
首殺 – (-:-)
解法說明
題目的要求是你要將一張無向圖的點分成兩個點集,
其中某些點要在點集 ,另一些點要在點集 。
而且要使得不跨點集的那些邊的權重和最大。
可以假設一開始可以獲得所有的經濟效益,
且把一個點分配在點集 ,則該點屬於源點這一邊;
如果分配在點集 則在匯點這邊。
題目的要求變成是要使得跨越點集那些邊的權重和最小,
則這道題目就變成最小割問題。
再考慮那些必定在點集 的那些點,
因為那些點不可以被分配到匯點的那一側,
所以從源點連接一條流量無限大的邊到那些點,
同理,必定在點集 的那些點,
則連接一條流量無限大的邊到匯點。
就可以跑一次從源點到匯點的最小割,
再用總經濟效益減掉最小割的值即為答案。
請注意:本題所有的邊都需要建雙向邊。
題外話:
如果題目沒有限制某些企業需要在某個廠區,
則此題會變成全域最小割問題,
有興趣的人可以自己上網搜尋解法。
標準程式
#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 305, S = 301, T = 302, INF = 1000000000;
struct MaxFlow{
struct edge{
int to, cap, flow, rev;
};
vector<edge> v[MAXN];
int s, t, dis[MAXN], cur[MAXN];
int dfs(int u, int cap){
if(u == t || !cap)
return cap;
for(int &i = cur[u]; i < v[u].size(); i++){
edge &e = v[u][i];
if(dis[e.to] == dis[u] + 1 && e.flow != e.cap){
int df = dfs(e.to, min(e.cap - e.flow, cap));
if(df){
e.flow += df;
v[e.to][e.rev].flow -= df;
return df;
}
}
}
dis[u] = -1;
return 0;
}
bool bfs(){
memset(dis, -1, sizeof(dis));
queue<int> q;
q.push(s), dis[s] = 0;
while(!q.empty()){
int tmp = q.front();
q.pop();
for(auto &u : v[tmp]){
if(!~dis[u.to] && u.flow != u.cap){
q.push(u.to);
dis[u.to] = dis[tmp] + 1;
}
}
}
return dis[t] != -1;
}
int maxflow(int _s, int _t){
s = _s, t = _t;
int flow = 0, df;
while(bfs()){
memset(cur, 0, sizeof(cur));
while(df = dfs(s, INF)){
flow += df;
}
}
return flow;
}
void addedge(int st, int ed, int cap){
v[st].push_back(edge{ed, cap, 0, v[ed].size()});
v[ed].push_back(edge{st, 0, 0, v[st].size() - 1});
}
} dinic;
int main() {
int n, m, k, x, a, b, r, sum = 0;
cin >> n >> m >> k;
while(k--){
cin >> x;
dinic.addedge(S, x, INF);
}
cin >> k;
while(k--){
cin >> x;
dinic.addedge(x, T, INF);
}
while(m--){
cin >> a >> b >> r;
sum += r;
dinic.addedge(a, b, r);
dinic.addedge(b, a, r);
}
cout << sum - dinic.maxflow(S, T) << "\n";
}
- Task Provider: arashi87
- Task Setter: arashi87
首殺 – (-:-)
解法說明
這題就是很單純的最短路而已,並沒卡特別的東西,唯一的難點就在於怎麼建邊而已,因為按照題目的規則在樓層間最多可以建到 條邊,因此樓層間的邊不能用常規的方法建。
所以在這題我們對於樓層間邊的建法需要用到一點小技巧,對於每層樓我們都多開一個虛點代表,兩層樓間需要相連的教室通通都連到這個虛點就行,這樣邊數就會從 退化到 ,但這樣還會有個問題是題目要求同樓層的教室不能相連,因此對於第 層樓連到 層的邊通通是單向邊,連到 的邊是雙向邊就能解決這問題了。
邊建完後跑一次最短路就行了。
標準程式
- Task Provider: XDEv11
- Task Setter: ys
首殺 team21_24 (03:23)
解法說明
我們先將 變為一樣大,再將不存在的邊補全,這題就是個很標準的匈牙利演算法的問題;至於不存在的邊該補成什麼,因為題目要求最大權最大匹配,我們可以將邊補成一個很大的負數,來確保轉化後的圖的最大權完美匹配,一定會是一個最大匹配。
標準程式
#include <ios>
#include <iostream>
#include <vector>
#include <queue>
#include <utility>
#include <algorithm>
using namespace std;
class WeightedBipartiteMatching {
int n;
vector<long long> lx{}, ly{};
vector<bool> vx{}, vy{};
queue<int> qu{};
vector<int> py{};
vector<long long> dy{};
vector<int> pdy{};
vector<int> mx{}, my{};
void relax(const vector<vector<long long>>& w, int x) {
for (int y{0}; y < n; ++y)
if (lx[x] + ly[y] - w[x][y] < dy[y])
dy[y] = lx[x] + ly[y] - w[x][y], pdy[y] = x;
}
void augment(int y) {
while (y != -1) {
int x{py[y]}, yy{mx[x]};
mx[x] = y, my[y] = x;
y = yy;
}
}
bool bfs(const vector<vector<long long>>& w) {
while (!qu.empty()) {
int x{qu.front()}; qu.pop();
for (int y{0}; y < n; ++y) {
if (!vy[y] && lx[x] + ly[y] == w[x][y]) {
vy[y] = true, py[y] = x;
if (my[y] == -1) return augment(y), true;
int xx{my[y]};
qu.push(x), vx[xx] = true, relax(w, xx);
}
}
}
return false;
}
void relabel() {
long long d{numeric_limits<long long>::max()};
for (int y{0}; y < n; ++y) if (!vy[y]) d = min(d, dy[y]);
for (int x{0}; x < n; ++x) if (vx[x]) lx[x] -= d;
for (int y{0}; y < n; ++y) if (vy[y]) ly[y] += d;
for (int y{0}; y < n; ++y) if (!vy[y]) dy[y] -= d;
}
bool expand(const vector<vector<long long>>& w) {
for (int y{0}; y < n; ++y) {
if (!vy[y] && dy[y] == 0) {
vy[y] = true, py[y] = pdy[y];
if (my[y] == -1) return augment(y), true;
int xx{my[y]};
qu.push(xx), vx[xx] = true, relax(w, xx);
}
}
return false;
}
public:
pair<vector<int>, vector<int>> operator()(const vector<vector<long long>>& w) {
n = w.size(), lx.assign(n, 0), ly.assign(n, 0), vx.resize(n), vy.resize(n), py.resize(n), dy.resize(n), pdy.resize(n), mx.assign(n, -1), my.assign(n, -1);
for (int i{0}; i < n; ++i)
for (int j{0}; j < n; ++j)
lx[i] = max(lx[i], w[i][j]);
for (int x{0}; x < n; ++x) {
fill(vx.begin(), vx.end(), false);
fill(vy.begin(), vy.end(), false);
fill(dy.begin(), dy.end(), numeric_limits<long long>::max());
qu = {}, qu.push(x), vx[x] = true, relax(w, x);
while (!bfs(w)) {
relabel();
if (expand(w)) break;
}
}
return {move(mx), move(my)};
}
};
void solve() {
const long long NONE{-1000000000};
int n, m;
cin >> n >> m;
vector<vector<long long>> w(max(n, m), vector<long long>(max(n, m), NONE));
for (int i{0}; i < m; ++i) {
int k;
cin >> k;
while (k--) {
int g, a;
cin >> g >> a, --g;
w[i][g] = a;
}
}
int ans{0};
auto mh{WeightedBipartiteMatching{}(w).first};
for (int i{0}; i < m; ++i) {
if (w[i][mh[i]] != NONE) ans += w[i][mh[i]];
}
cout << ans << '\n';
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t{1};
while (t--) solve();
return 0;
}
- Task Provider: GY
- Task Setter: ys
首殺 team21_24 (01:48)
解法說明
問題是在求
枚舉每個區間,並利用重疊的區間減少計算量,其複雜度為
例如計算 可用已經算過的 來算
由於這種作法複雜度過高,需要再仔細觀察問題特性,以減少計算量
令
會發現
並且對任意的 最多只會有 個相異的
因為一個數 的質因數個數不超過 個
於是對每個 找出這些相異數 且乘上對應的數量,接著加總起來就行
解法 1
對於每個
在 找到所有 個位置 滿足
將這些相異數一個個跟 取 GCD,會得到
若 有 則保留其中一個,這樣就得到對於 的所有相異數
實作上對於 用陣列 num
存相異數 、陣列 cnt
存對應的個數
而 _num
及 _cnt
分別會暫存對於 的相異數 及其對應的個數
解法 2
對於每個 有
對任意 能找到 的 lower bound 以及 upper bound
且可以順便得知下個相異於 的數的位置
實作上對於任意 採用 Sparse Table 查詢 的值,並用二分搜找出 bound
標準程式
解法 1
解法 2
- Task Provider: ccbeginner
- Task Setter: ccbeginner
首殺 – (-:-)
解法說明
先把題目給出的矩陣變成一個二分圖,假設有兩個子圖 , 有 個節點, 有 個節點,然後如果第 行第 列的數字大於 ,就把 跟 連一條邊起來。
然後……就是求整張圖的最小生成樹啦~
最小生成樹上的錢錢數量就是大人拿到的數量。
最後再把圖上權重和減掉最小生成樹上的權重和就是答案。
標準程式