T1.Deliver the Cake
分析题意
首先,如果忽略掉蛋糕拿在手上的不同状态,那么实际就是求最短路问题
但是,在每个村庄,拿蛋糕的状态我们必须考虑进去,所以将每个点的情况枚举,并按照不同的转移方式连边,就是我们需要做的
实现:
拆点
题目给出了ll, rr, mm三种不同的节点
ll, rr :对于每个 ll 或者 rr点,状态是确定的,故拆分成
ll : (ll, ll)
rr : (rr, rr)
mm : 将 mm 点拆分成 ll 和 rr 两个节点,并在图中建立相应的边
mm : (ll, rr)
连边
需要注意的是,我们将点拆分,实际拆分的是点的状态,也就是说类似的创造了一个新点,只不过同属于一个村庄的两种不同状态
所以,在连边时,实际有四种状态的边相连(每个点各有两种)
建立无向图,注意双向存边
代码实现
初始化
cnt = 0;
ans = INF;
拆点
for (int i = 1; i <= n; i++) {
if (sc[i] == 'L')
node[i] = 'L', node[i + n] = 'L';
if (sc[i] == 'R')
node[i] = 'R', node[i + n] = 'R';
if (sc[i] == 'M')
node[i] = 'L', node[i + n] = 'R';
}
存边
for (int i = 1; i <= m; i++) {
int u, v;
long long w;
u = read();
v = read();
w = read();
add(u, v, w);
add(u, v + n, w);
add(u + n, v, w);
add(u + n, v + n, w);
}
void add(int x, int y, long long z) {
ver[++cnt] = y;
edgew[cnt] = z;
nextw[cnt] = head[x];
head[x] = cnt;
// 双向存边
ver[++cnt] = x;
edgew[cnt] = z;
nextw[cnt] = head[y];
head[y] = cnt;
}
dijkstradijkstra
void dijkstra(int s) {
for (int i = 0; i <= (n << 1); i++) {
dis[i] = INF;
vis[i] = 0;
}
dis[s] = 0;
q.push(edge(0, s));
while (q.size()) {
int x = q.top().id;
q.pop();
if (vis[x])
continue;
vis[x] = 1;
for (int i = head[x]; i != -1; i = nextw[i]) {
int y = ver[i];
int z = edgew[i];
int u;
if (node[x] == node[y])
u = 0;
else
u = 1;
if (dis[y] > dis[x] + z + u * cost) {
dis[y] = dis[x] + z + u * cost;
q.push(edge(dis[y], y));
}
}
}
}
处理最短路
因为起点与终点的状态也被拆分,所以需要分类讨论
dijkstra(s);
ans = min(ans, dis[t]);
ans = min(ans, dis[t + n]);
dijkstra(s + n);
ans = min(ans, dis[t]);
ans = min(ans, dis[t + n]);
write(ans);
printf("\n");
最后,注意数据范围,双向存边
T2.Very Easy Graph Problem
题意分析
题目中给出第ii条边的权值为2^i2i是一个关键的突破点
由数学知识我们可以得到:\sum_1^k∑1k 2 ^ i2i < (2 ^ k2k * 22)
所以,对于输入的一条边(u, v)(u,v)的两个端点u, vu,v, 如果之前的边满足使它们联通,这条边的权值将不作贡献
由此,对于所有的起点ss, 终点tt(a[s] == 1a[s]==1 && a[t] == 0a[t]==0)所构成的一张图,是满足最小生成树的性质的,即所有的点任意联通,且满足权值总和最小
具体实现
那么,接下来,我们就需要统计最小生成树上每条边做出了贡献的次数
假设有一条边为(u, v)(u,v), uu为vv的父亲节点
记录以vv根节点的子树中00的个数为b[v].p0b[v].p0,b[v].p1b[v].p1同理
记录sum0sum0为整棵树中00的个数,sum1sum1同理
所以,这条边对答案的贡献就是
sum = sum + (sum0 - b[i].p0) * b[i].p1 * b[i].w;
sum %= mod;
sum = sum + (sum1 - b[i].p1) * b[i].p0 * b[i].w;
sum %= mod;
文字描述: 以vv为根节点的子树中00的个数 乘上 子树以外11的个数, 再加上 以vv为根节点的子树中11的个数 乘上 子树以外00的个数
代码实现
#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
#define ll long long
const int N = 2e5 + 5, mod = 1e9 + 7;
int p, n, m, k, sum0, sum1;
int a[N], fa[N];
ll val;
struct node {
int p0, p1;
ll w;
};
node b[N];
struct node operator+(struct node x, struct node y) {
struct node z;
z.p0 = x.p0 + y.p0;
z.p1 = x.p1 + y.p1;
z.w = 0;
return z;
}
vector<pair<int, ll> > e[N];
int find(int x) {
if (fa[x] == x)
return x;
return fa[x] = find(fa[x]);
}
struct node dfs(int now, int pre) {
int size = e[now].size();
pair<int, ll> pa;
int temp = k;
k++;
ll w = 0;
for (int i = 0; i < size; i++) {
pa = e[now][i];
if (pa.first == pre) {
w = pa.second;
continue;
}
b[temp] = b[temp] + dfs(pa.first, now);
}
b[temp].w = w;
if (a[now] == 0)
b[temp].p0++;
else
b[temp].p1++;
return b[temp];
}
int main() {
scanf("%d", &p);
while (p--) {
val = 1;
sum0 = sum1 = a[0] = k = 0;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
fa[i] = i;
if (a[i] == 1)
sum1 += 1;
if (a[i] == 0)
sum0 += 1;
b[i].p0 = b[i].p1 = b[i].w = 0;
e[i].clear();
}
int u, v;
for (int i = 1; i <= m; i++) {
scanf("%d %d", &u, &v);
val *= 2;
val %= mod;
if (find(u) == find(v))
continue;
fa[find(u)] = find(v);
e[u].push_back(make_pair(v, val));
e[v].push_back(make_pair(u, val));
}
dfs(1, -1);
ll sum = 0;
for (int i = 1; i < n; i++) {
sum = sum + (sum0 - b[i].p0) * b[i].p1 * b[i].w;
sum %= mod;
sum = sum + (sum1 - b[i].p1) * b[i].p0 * b[i].w;
sum %= mod;
}
printf("%lld\n", sum);
}
return 0;
}
我认为这道题应该提出来分析的内容
有关代码重点部分dfsdfs,我的一些思考
struct node dfs(int now, int pre) {
int size = e[now].size();
pair<int, ll> pa;
int temp = k;
k++;
ll w = 0;
// printf("%d %d %d\n", now, pre, temp);
for (int i = 0; i < size; i++) {
pa = e[now][i];
// 这条边连接到当前点的父节点,记录下w,即题意分析中提及的:
// 当有边(u, v)在最小生成树中时,以v为根节点,讨论(u, v)这条边对答案的贡献
if (pa.first == pre) {
w = pa.second;
continue;
}
// 沿着祖孙关系,向下dfs搜索,直至将整个子树的所有(0, 1)情况累加
b[temp] = b[temp] + dfs(pa.first, now);
}
b[temp].w = w;
// 因为,当前遍历到的now节点,本身也属于其祖先节点的子树中的一部分
// 所以要注意将now节点本身的情况统计进入数组,在向上回溯时,传递答案
if (a[now] == 0)
b[temp].p0++;
else
b[temp].p1++;
return b[temp];
}
另外,temp是在记录每一条边的相关信息,作为我们统计答案的标识
ps:因为是树的搜索,每条边恰好被访问一次(本人最开始搞不太明白这个操作)
在统计答案时,不难发现我们枚举的点是11到n - 1n−1,也就是说,最后统计的是建立nn个点的最小生成树所需的n - 1n−1条边,每条边作出的贡献和
for (int i = 1; i < n; i++) {
sum = sum + (sum0 - b[i].p0) * b[i].p1 * b[i].w;
sum %= mod;
sum = sum + (sum1 - b[i].p1) * b[i].p0 * b[i].w;
sum %= mod;
}
以下是,合并操作——运算符重载
struct node operator+(struct node x, struct node y) {
struct node z;
z.p0 = x.p0 + y.p0;
z.p1 = x.p1 + y.p1;
z.w = 0;
return z;
}