最小生成树
387. 北极网络
题目链接
直接二分距离
m
i
d
mid
mid,kruscal过程中两点距离小于
m
i
d
mid
mid即可连边。最后并查集数量小于
s
s
s说明
m
i
d
mid
mid可行,否则增大
m
i
d
mid
mid。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 510;
typedef pair<int, int> PII;
int s, n;
int x[N], y[N], fa[N];
struct E{
int u, v;
double dis;
bool operator < (const E &x) const {return dis < x.dis;}
};
vector<E> e;
inline double dis(int i, int j){return sqrt(pow(x[i] - x[j], 2) + pow(y[i] - y[j], 2));}
int ff(int x){return fa[x] == x ? x : fa[x] = ff(fa[x]); }
bool check(double mid)
{
for(int i = 1; i <= n; i ++) fa[i] = i;
int all = n;
for(auto it : e)
{
double dis = it.dis;
if(dis > mid) break;
int u = it.u, v = it.v;
u = ff(u), v = ff(v);
if(u == v) continue;
all --;
fa[u] = v;
}
return all <= s;
}
int main()
{
int t; scanf("%d", &t);
while(t --)
{
e.clear();
scanf("%d%d", &s, &n);
for(int i = 1; i <= n; i ++)
scanf("%d%d", &x[i], &y[i]);
if(s == n)
{
puts("0.00");
continue;
}
for(int i = 1; i <= n; i ++)
for(int j = i + 1; j <= n; j ++)
e.push_back({i, j, dis(i, j)});
sort(e.begin(), e.end());
double l = 0, r = 3e4, mid;
while(r - l >= 1e-5)
{
mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid;
}
printf("%.2f\n", mid);
}
return 0;
}
388. 四叶草魔杖
题目链接
枚举连通且和为0的每个点集
s
s
s,每个点集
s
s
s内的最小代价即其生成树。然后再将点集合并:两个点集不相交,则它们两个可用去更新并集点集。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 20, inf = 0x3f3f3f3f;
int g[N][N], a[N], n, m, f[1 << 20];
bool vis[N];
struct E{
int u, v, w;
bool operator < (const E &x) const {return w < x.w;}
};
vector<E>e;
int fa[N];
int ff(int x){return x == fa[x] ? x : fa[x] = ff(fa[x]); }
int kruscal(int sz)
{
int ans = 0;
for(auto it : e)
{
int u = it.u, v = it.v, w = it.w;
if(!vis[u] || !vis[v]) continue;
u = ff(u), v = ff(v);
if(u == v) continue;
fa[u] = v, ans += w, sz --;
}
return sz == 1 ? ans : inf;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for(int i = 0; i < n; i ++) cin >> a[i];
for(int i = 1; i <= m; i ++)
{
int u, v, w; cin >> u >> v >> w;
g[u][v] = g[v][u] = w;
e.push_back({u, v, w});
}
int mx = (1 << n) - 1;
sort(e.begin(), e.end());
memset(f, 0x3f, sizeof f);
if(!e.size())
{
int sum = 0;
for(int i = 0; i < n; i ++)
{
if(a[i] != 0) {
puts("Impossible"); return 0;
}
}
puts("0"); return 0;
}
for(int s = 0; s <= mx; s ++)
{
memset(vis, 0, sizeof vis);
int sum = 0, sz = 0;
for(int i = 0; i < n; i ++)
if((s >> i) & 1)
vis[i] = true, fa[i] = i, sum += a[i], sz ++;
if(sum == 0)
f[s] = kruscal(sz);
}
if(f[mx] == 0) {
puts("0"); return 0;
}
for(int i = 0; i <= mx; i ++)
{
if(f[i] == inf) continue;
for(int j = 0; j <= mx; j ++)
{
if(f[j] == inf || (i & j)) continue;
f[i | j] = min(f[i | j], f[i] + f[j]);
}
}
if(f[mx] == inf) puts("Impossible");
else printf("%d\n", f[mx]);
return 0;
}
树的直径
389. 直径
题目链接
找直径上的必经边。必经边必然是一条链,不可能交叉(交叉说明被不同的直径经过)。先
d
f
s
dfs
dfs获得一条直径,记录直径的起点
s
t
st
st终点
e
d
ed
ed,和直径边上到起点的距离
d
i
s
[
i
]
dis[i]
dis[i]。从起点向终点枚举,找到一个最靠近终点的直径上的点
l
l
l,它可以不经过直径上的边而找到另一条路径长度为
d
i
s
[
l
]
dis[l]
dis[l],则说明另一条路径和
l
l
l~
e
d
ed
ed构成了一条新直径,则
s
t
st
st到
l
l
l路径上的都不是必经边。然后从终点向起点枚举,同意找到一个
r
r
r,
r
r
r到
e
d
ed
ed的边均不是必经边。则
l
l
l ~
r
r
r剩下的边均是必经边。
note: 下面是挺完备的板子哟,什么东西都记录下来,而且不很长。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 2e5 + 10;
int n;
struct E{
int to, nxt, w;
bool is;
}e[N << 1];
int head[N], cnt = 1;
void add(int u, int v, int w)
{
e[++ cnt] = {v, head[u], w, 0};
head[u] = cnt;
}
LL mx = 0;
int st, ed;
void dfs1(int u, int fa, LL dep, int &ed, LL &mx)
{
if(dep > mx) mx = dep, ed = u;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(v == fa) continue;
dfs1(v, u, dep + e[i].w, ed, mx);
}
}
bool is[N];
LL dis[N];
int ord[N], d_cnt;
bool dfs2(int u, int fa, int st, LL &mx)
{
if(u == st)
{
dis[u] = 0;
ord[++ d_cnt] = u;
return is[u] = true;
}
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(v == fa) continue;
if(dfs2(v, u, st, mx))
{
ord[++ d_cnt] = u;
mx = dis[u] = mx + e[i].w;
return e[i].is = e[i ^ 1].is = is[u] = true;
}
}
}
LL dfs3(int u, int fa)
{
LL mx = 0;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(e[i].is || v == fa) continue;
mx = max(mx, 1ll * e[i].w + dfs3(v, u));
}
return mx;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i < n; i ++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w), add(v, u, w);
}
dfs1(1, 0, 0, st, mx);
mx = 0, dfs1(st, 0, 0, ed, mx);
mx = 0, dfs2(ed, 0, st, mx);
int l = 1, r = d_cnt;
for(int i = 2; i <= d_cnt; i ++)
if(dfs3(ord[i], 0) == dis[ord[i]]) l = i;
for(int i = d_cnt - 1; i >= 2; i --)
if(dfs3(ord[i], 0) == dis[ed] - dis[ord[i]]) r = i;
int ans = 0;
printf("%lld\n%d\n", mx, r - l);
return 0;
}
lca
391. 聚会
题目链接
在树上找到3个点相聚的最短距离。先找出两个点的
l
c
a
lca
lca,作为相聚的节点,再计算另一个点到此
l
c
a
lca
lca的距离。以上过程重复3次取最大值。不可直接找到3个点的
l
c
a
lca
lca,然后计算每个点到公共
l
c
a
lca
lca的距离,这样会导致有一些边走两次。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 5e5 + 10, inf = 0x3f3f3f3f;
int n, m;
struct E{
int to, nxt;
}e[N << 1];
int head[N], cnt = 1, all;
int dep[N], f[N][20];
void add(int u, int v)
{
e[++ cnt] = {v, head[u]};
head[u] = cnt;
}
void dfs(int u, int ff)
{
f[u][0] = ff;
dep[u] = dep[ff] + 1;
for (int i = 1; i <= all; i ++ )
f[u][i] = f[f[u][i - 1]][i - 1];
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(v == f[u][0]) continue;
dfs(v, u);
}
}
int lca(int u, int v)
{
if(dep[u] > dep[v]) swap(u, v);
for(int i = all; i >= 0; i --)
if(dep[f[v][i]] >= dep[u]) v = f[v][i];
if(u == v) return u;
for(int i = all; i >= 0; i --)
if(f[v][i] != f[u][i]) u = f[u][i], v = f[v][i];
return f[u][0];
}
int cal(int a, int b, int c, int &ab_lca)
{
ab_lca = lca(a, b);
int d = lca(ab_lca, c);
int ans = dep[a] + dep[b] - 2 * dep[ab_lca];
if(d == c || d == ab_lca) ans += abs(dep[ab_lca] - dep[c]);
else ans += dep[c] + dep[ab_lca] - 2 * dep[d];
return ans;
}
int main()
{
scanf("%d %d", &n, &m);
all = (int) (log(n) / log(2)) + 1;
for(int i = 1; i < n; i ++)
{
int u, v; scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
dfs(1, 0);
int pos, cost;
while(m --)
{
int a, b, c; scanf("%d%d%d", &a, &b, &c);
int t1, t2;
cost = cal(a, b, c, pos);
t1 = cal(a, c, b, t2);
if(t1 < cost) cost = t1, pos = t2;
t1 = cal(b, c, a, t2);
if(t1 < cost) cost = t1, pos = t2;
printf("%d %d\n", pos, cost);
}
return 0;
}
差分约束
393. 雇佣收银员
题目链接
此题的约束为:前7小时+当前小时的开始上班人数不少于当前的小时需要的上班人数。因此可定义前缀和约束:(时间从1-24)设
s
[
i
]
s[i]
s[i]为从第
1
1
1个小时到第
i
i
i个小时开始上班的总人数。
当
i
>
=
8
i>=8
i>=8,
s
[
i
]
−
s
[
i
−
8
]
>
=
r
[
i
]
s[i]-s[i-8]>=r[i]
s[i]−s[i−8]>=r[i];而当
i
<
8
i<8
i<8则还需要加上第一个小时之前的,即从第24小时向前数,有
s
[
i
]
+
s
[
24
]
−
s
[
24
−
(
8
−
i
)
]
>
=
r
[
i
]
s[i]+s[24]-s[24-(8-i)]>=r[i]
s[i]+s[24]−s[24−(8−i)]>=r[i]。再加上基本约束:每个小时开始上班人数少于申请人数,且不为负数。
note: spfa出队则vis设为0;约束某个变量x为某个常量a,要加上
a
<
=
x
<
=
a
a<=x<=a
a<=x<=a的两个约束;看清题意;差分约束需要考虑基本约束条件和题目特定约束条件。
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
int n = 24;
int s[N], num[N], r[N], q_num[N];
bool vis[N];
struct E{
int to, nxt, w;
}e[N * N];
int head[N], cnt;
void add(int u, int v, int w)
{
e[++ cnt] = {v, head[u], w};
head[u] = cnt;
}
bool spfa()
{
memset(s, -0x3f, sizeof s);
memset(q_num, 0, sizeof q_num);
memset(vis, 0, sizeof vis);
queue<int> q;
q.push(0); vis[0] = true; q_num[0] = 1; s[0] = 0;
while(q.size())
{
int u = q.front(); q.pop();vis[u] = false;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(s[v] < s[u] + e[i].w)
{
s[v] = s[u] + e[i].w;
if(!vis[v])
{
vis[v] = true;
q_num[v] ++;
if(q_num[v] > n) return false;
q.push(v);
}
}
}
}
return true;
}
bool check(int mid)
{
cnt = 0;
memset(head, 0, sizeof head);
add(0, n, mid), add(n, 0, - mid);
for(int i = 1; i <= n; i ++)
{
add(i - 1, i, 0), add(i, i - 1, - num[i]);
if(i >= 8) add(i - 8, i, r[i]);
else add(16 + i, i, r[i] - mid);
}
return spfa();
}
int main()
{
int t; scanf("%d", &t);
while(t --)
{
memset(num, 0, sizeof num);
for(int i = 1; i <= n; i ++) scanf("%d", &r[i]);
int m; scanf("%d", &m);
for(int i = 1; i <= m; i ++)
{
int tmp; scanf("%d", &tmp);
num[tmp + 1] ++;
}
int l = 0, r = m, mid, ans;
while(l < r)
{
int mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid + 1;
}
if(check(l)) printf("%d\n", l);
else puts("No Solution");
}
return 0;
}
2-sat
404. 婚礼
将每个人编号为0~2n-1,第i对夫妻的妻子为i,丈夫为i+n。再将每个人i拆成两个点i和i+2n,i表示和新娘同侧,i+2n表示和新娘异侧。接下来看约束条件:添加连边(i, j)表示j和新娘同侧时,j必须和新娘同侧;添加连边(i, j+2n)表示i和新娘同侧时,j必须和新娘异侧,(i+2n,j),(i+2n,j+2n)连边含义类似。
- 新郎在异侧:add(0,n+2n)
- 夫妻在异侧:
- 妻子和新娘同侧:add(i, i + 3n)
- 丈夫和新娘同侧:add(i + n, i + 2n)
- 妻子和新娘异侧:add(i + 2n, i + n)
- 丈夫和新娘异侧:add(i + 3n, i)
- 淫乱不被新娘看到:即不能同时在新娘异侧
在tarjan缩点之后如果有i和i+2n在同一个强连通分量里,则产生矛盾无解。
否则至少有一组解,在拓扑序列中,一个人拓扑序更大的点表示的情况必然可行。但是此题要求在同侧和在异侧的人的数量相同,因此可进行dfs,看一个人的一种情况a可否导向另一种情况b,若可导向则说明此人只能是情况b,否则ab两种情况均可,在最后再对此人进行分配以使得两侧人数相同。