2020.11.25 选拔赛题解
A (最短路变形)
⭐⭐
题意:给出一张图,图中的节点分为黑点白点,求出黑点对之间的最短距离并输出这两个黑点的编号
解析:
- 通过题目中关键字最短距离,大体可以想到为求最短路,但是这里是把所有的点分为了两类,求黑点之间的最短距离。
- 考虑到dijkstra算法可以算出单源最短路,但是如果在初始化优先队列时,将同类型的点一并push入队列中,便可以得到多源最短路(本题中为各点到黑点的最近距离),这样处理的复杂度为\(O((m+n)\log n)\)
- 对于一段从黑点到黑点的路径,可以转化为下述这样的情况
那么假设一个点为\(a\),另一个点为\(b\),如若这两个点的最近黑点不是一个点,那么就考虑所有这样的\(<a,b>\)组合,所组成路径的最小值即为答案,即复杂度为暴力枚举所有边\(O(m)\)
注意:\(pre\)一定要是黑点才行,所以一开始定义所有的\(pre\)都是-1,遇到是-1的点直接跳过他的边枚举
总结:
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
const long long INF = 0x3f3f3f3f3f3f3f3fll;
const int maxn = 1e5 + 10;
typedef pair <long long, int> P;
int n, m;
int cnt, sign[maxn];;
bool vis[maxn];
vector<P> e[maxn];
vector<int> pre;
vector<long long> d;
priority_queue<P, vector<P>, greater<P>> q;
void dij()
{
pre.assign(n + 1, -1), d.assign(n + 1, INF);
for (int i = 1; i <= n; ++i)
if (sign[i])
pre[i] = i, d[i] = 0, q.push(P(d[i], i));
while (!q.empty())
{
P t = q.top(); q.pop();
if (d[t.second] < t.first) continue;
for (auto& i : e[t.second])
{
if (d[i.second] > d[t.second] + i.first)
{
d[i.second] = d[t.second] + i.first;
q.push(P(d[i.second], i.second));
pre[i.second] = pre[t.second];
}
}
}
}
void add(int u, int v, int c)
{
e[u].push_back(P(c, v));
e[v].push_back(P(c, u));
}
int main()
{
int a, b, c;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", &sign[i]);
while (m--)
{
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
dij();
long long ret = 0x3f3f3f3f3f3f3f3fll;
for (int i = 1; i <= n; ++i)
{
if (pre[i] == -1) continue;
for (auto& j : e[i])
if (~pre[j.second] && pre[i] != pre[j.second] && d[i] + d[j.second] + j.first < ret)
ret = d[i] + d[j.second] + j.first, a = pre[i], b = pre[j.second];
}
if (ret >= 0x3f3f3f3f3f3f3f3fll)
printf("No luck at all");
else
printf("%lld\n%d %d", ret, a, b);
}
B (区间dp)
⭐⭐
题意:
你有\(n\)个任务需要完成,任务按照时间顺序交付到你的手上,你在任意时刻可以花费\(d\)完成之前所有未完成的任务,但是当每一个任务被延迟完成一个单位时间会花费\(c\),请问完成这些任务最少花费多少
解析:
- 定义\(dp[i]\)状态为完成第\(i\)个任务所需要花费的最小时间,\(time[i]\)为第\(i\)个任务的交付时间,不难得到状态转移方程
- 但是如果朴素的去统计求和部分,复杂度会达到\(O(n^3)\),明显会超时,所以考虑将上述转移方程进行化简
这样的话求和部分就可以用前缀和进行\(O(1)\)获取了
注意:
- 不开\(long\ long\)必WA
- 第二步的化简让公式更简洁
#include<cstdio>
#include<string>
#include<algorithm>
#define MEM(X,Y) memset(X,Y,sizeof(X))
typedef long long LL;
using namespace std;
/*===========================================*/
LL dp[1005], sum[1005];
LL dat[1005];
int main(void)
{
int n, d, c;
scanf("%d%d%d", &n, &d, &c);
for (int i = 1; i <= n; ++i)
{
scanf("%lld", &dat[i]);
sum[i] = sum[i - 1] + dat[i];
}
MEM(dp, INF);
dp[0] = 0;
for (int i = 1; i <= n; ++i)
for (int j = 0; j < i; ++j)
dp[i] = min(dp[i], dp[j] + c * ((1LL * i - j) * dat[i] - sum[i] + sum[j]) + d);
printf("%lld", dp[n]);
}
C (区间dp)
⭐⭐⭐⭐
题意:
给\(n\)个整数,定义一个数是有序的当且仅当它左边的数都小于等于它,它右边的数都大于等于它,排列这\(n\)个整数能组成多少个序列使得所有数都是无序的(答案模取\(10^9+9\))
解析:
- 如果将从\([l,r]\)的数的不重复排列个数记为\(permu[l][r]\),\(dp[i]\)定义为从\(1\sim i\)的数据构成无序序列的个数,那么可以考虑问题的反面,无序数列的个数,等于这些所有数的全排列减去至少有一个数保持有序性对应的排列数
- 为了不重不漏的寻找到长度为\(n\)的序列至少有一个数保持有序性对应的排列数,考虑枚举第\(i\)个数为第一个有序的数,需要保证在这个数左边的部分\([1,i)\)均是小于它的且均为无序数列,而右边的部分\((i,n]\)只要是大于\(dat[i]\)即可
- 而对于左边和右边的相对于\(dat[i]\)的偏序关系,可以进行\(sort\)来实现
- 这样问题转化成了如何求出\([l,r]\)数构成的不重复排列个数,很明显这个值等于
\(c\)为区间内每个数对应的个数,去除选择先后性带来的影响
很明显可以发现这个式子可以进行递推记录即
总结:
注意:
- \(permu\)在递推过程中要访问到\(permu[l][l-1]\)(即区间内只有一个数)这个量,所以应该提前赋值为1
- \(dp\)递推过程中也要访问到\(permu[i+1][i]\)这个量(即右端点的那个值为第一个有序的,也就是右端点是最大的),这与上述第一点的赋值相对应(最高赋值到了\(permu[n][n-1]\)),所以应该再多赋值一个\(permu[n+1][n]=1\)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 5e3 + 3, mod = 1e9 + 9;
LL inv[maxn], permu[maxn][maxn];
LL dat[maxn];
LL dp[maxn];
map<int, int> m;
int main()
{
int n;
scanf("%d", &n);
inv[1] = 1;
for (int i = 2; i <= n; ++i)
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for (int i = 1; i <= n; ++i)
scanf("%lld", &dat[i]);
sort(dat + 1, dat + n + 1);
for (int i = 1; i <= n; ++i)
{
m.clear();
permu[i][i - 1] = 1;
for (int j = i; j <= n; ++j)
permu[i][j] = permu[i][j - 1] * (j - i + 1) % mod * inv[++m[dat[j]]] % mod;
}
permu[n + 1][n] = 1;
dp[0] = 1;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= i; ++j)
dp[i] = (dp[i] + dp[j - 1] * permu[j + 1][i] % mod) % mod;
dp[i] = ((permu[1][i] - dp[i]) % mod + mod) % mod;
}
printf("%lld", dp[n]);
}
D (网络流+二分离散化+状态压缩)
⭐⭐⭐⭐
题目链接
题意:
有\(n\)个城市\(m\)条道路,道路是双向的,其中有\(s\)个城市是安全点,每个城市中都有\(p[i]\)居民,每个安全点可容纳的居民为\(C[i]\),请问所有城市的居民都到达安全点的最小时间是多少,保证有解
解析:
- 由题意很明显可以看出是一道网络流,问题需要得到\(time_{min}\),而这个值显然可以通过二分进行获取,用网络流判断最大流是否等于\(sum_{p}\)进行检验
- 建图方式可以采用下述办法
但是这道题目中输出流量的城市和最终最大流没有直接关系,因此可以将所有流向同样安全点的城市汇总成一个状态\(st\),这样的话点数从\(10^5\)降低到\(2^{10}=1024\)
3. 同时需要进行将城市与状态相匹配的判断,这样就得预先对每个安全点跑一遍\(dijkstra\),记录每个安全点到每个城市的最小距离,对于每个城市\(i\),检测它到每个安全点的最小距离是否小于当前检测的\(time\),可以则将\(i\)归于状态\(st\),\(st\)中增加对应的人口数量
4. 为了降低复杂度,还可以在二分的范围上进行优化。可以将\(dijkstra\)出现的所有可能时间进行记录后,离散化处理。不同时间的种类数最大为\(10^6\),这样二分的时间复杂度就从\(\log_210^9\approx50\)降低到\(\log_210^6\approx20\)
注意:
- 与流量相关的数据都必须要开\(long\ long\),因为总流量最大值为\(10^{14}\)
总结:
- 先对安全点跑最短路,对后面的连边检测做好预处理,同时记录所有时间,进行离散化处理
- 在离散化获得的区间进行二分查找
- 对相同状态的城市进行合并,连边,跑\(dinic\)
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define FRE freopen("abc.in", "r", stdin)
#define MEM(X,Y) memset(X,Y,sizeof(X))
typedef long long LL;
using namespace std;
/*===========================================*/
typedef long long LL;
typedef pair<LL, int> P;
LL sum;
int people[100005];
P saf[15];
LL d[15][100005];
LL st[1050];
vector<pair<LL, int>> e[100005];
priority_queue<P, vector<P>, greater<P> > q;
void dij(int x)
{
int id = saf[x].second;
MEM(d[x], INF);
d[x][id] = 0;
q.push(P(0, id));
while (!q.empty())
{
P p = q.top(); q.pop();
int v = p.second;
if (d[x][v] < p.first) continue;
for (auto& i : e[v])
{
if (d[x][i.second] > d[x][v] + i.first)
{
d[x][i.second] = d[x][v] + i.first;
q.push(P(d[x][i.second], i.second));
}
}
}
}
//使用非vector链式前向星
class MAXFLOW
{
public:
static const int MAXN = 100005;
struct Edge
{
int v, next;
LL flow;
} e[MAXN * 50];
int head[MAXN], edge_num, layer[MAXN], start, end;
void reload()
{
edge_num = 0;
memset(head, -1, sizeof(head));
memset(st, 0, sizeof(st));
}
void addedge(int u, int v, LL w)
{
e[edge_num].v = v;
e[edge_num].flow = w;
e[edge_num].next = head[u];
head[u] = edge_num++;
e[edge_num].v = u;
e[edge_num].flow = 0;
e[edge_num].next = head[v];
head[v] = edge_num++;
}
bool bfs()
{
queue<int> Q;
Q.push(start);
memset(layer, 0, sizeof(layer));
layer[start] = 1;
while (Q.size())
{
int u = Q.front();
Q.pop();
if (u == end)
return true;
for (int j = head[u]; j != -1; j = e[j].next)
{
int v = e[j].v;
if (layer[v] == false && e[j].flow)
{
layer[v] = layer[u] + 1;
Q.push(v);
}
}
}
return false;
}
LL dfs(int u, LL MaxFlow, int End)
{
if (u == End)
return MaxFlow;
LL uflow = 0;
for (int j = head[u]; j != -1; j = e[j].next)
{
int v = e[j].v;
if (layer[v] - 1 == layer[u] && e[j].flow)
{
LL flow = min(MaxFlow - uflow, e[j].flow);
flow = dfs(v, flow, End);
e[j].flow -= flow;
e[j ^ 1].flow += flow;
uflow += flow;
if (uflow == MaxFlow)
break;
}
}
if (uflow == 0)
layer[u] = 0;
return uflow;
}
LL dinic()
{
LL MaxFlow = 0;
while (bfs())
MaxFlow += dfs(start, 0x3f3f3f3f3f3f3f3f, end);
return MaxFlow;
}
}sol;
int n, m, s;
vector<LL> tim;
bool check(LL time)
{
sol.reload();
sol.start = 0;
for (int i = 1; i <= n; ++i)
{
int t = 0;
for (int j = 1; j <= s; ++j)
if (d[j][i] <= time)
t |= 1 << (j - 1);
st[t] += people[i];
}
int mx = 1 << s;
for (int i = 1; i < mx; ++i)
{
sol.addedge(sol.start, i, st[i]);
if (!st[i]) continue;
for (int j = 1; j <= s; ++j)
if (i & (1 << (j - 1)))
sol.addedge(i, mx + j, INF);
}
sol.end = mx + s + 1;
for (int i = 1; i <= s; ++i)
sol.addedge(i + mx, sol.end, saf[i].first);
return sol.dinic() == sum;
}
int main(void)
{
int a, b;
LL c;
scanf("%d%d%d", &n, &m, &s);
for (int i = 1; i <= n; ++i)
scanf("%d", &people[i]), sum += people[i];
while (m--)
{
scanf("%d%d%lld", &a, &b, &c);
e[a].push_back(P(c, b));
e[b].push_back(P(c, a));
}
for (int i = 1; i <= s; ++i)
{
scanf("%d%lld", &a, &c);
saf[i] = P(c, a);
dij(i);
for (int j = 1; j <= n; ++j)
tim.push_back(d[i][j]);
}
sort(tim.begin(), tim.end());
tim.erase(unique(tim.begin(), tim.end()), tim.end());
int l = 0, r = tim.size() - 1;
while (l < r)
{
int m = l + (r - l) / 2;
if (check(tim[m])) r = m;
else l = m + 1;
}
printf("%lld", tim[r]);
}