总结:生成树的知识点真多,不过博主觉得图论的题目终究是建图难,第一步就是如何建图,将其转换成已知的问题。
另外,关于生成树的两个注意点,也是为了防止碰到毒瘤题。一就是自环,二就是重边。
A - The Unique MST
次小生成树裸题。
prim模板
#include
#include
#include
#include
using namespace std;
const int maxn = 105;
const int inf = 0x3f3f3f3f;
int map[maxn][maxn];
int lowcost[maxn];
int closest[maxn];
bool used[maxn][maxn];
bool vis[maxn];
int Max[maxn][maxn];
int n, m;
int prim()
{
memset(lowcost, inf, sizeof(lowcost));
memset(closest, 0, sizeof(closest));
memset(used, 0, sizeof(used));
memset(vis, 0, sizeof(vis));
memset(Max, 0, sizeof(Max));
lowcost[1] = 0;
vis[1] = 1;
closest[1] = 1;
int num = 0, ans = 0;
int nownode = 1;
while (num1)
{
int Mincost = inf, theidx;
for (int i = 1;i <= n;i++)
{
if (vis[i])
{
if (i == nownode)continue;
Max[i][nownode] = Max[nownode][i] = max(Max[i][closest[nownode]], lowcost[nownode]);
}
else
{
if (lowcost[i] > map[i][nownode])
{
lowcost[i] = map[i][nownode];
closest[i] = nownode;
}
if (Mincost > lowcost[i])
{
Mincost = lowcost[i];
theidx = i;
}
}
}
ans += Mincost;
num++;
nownode = theidx;
vis[nownode] = 1;
used[theidx][closest[theidx]] = used[closest[theidx]][theidx] = 1;
}
for (int i = 1;i <= n;i++)
Max[i][nownode] = Max[nownode][i] = max(Max[i][closest[nownode]], lowcost[nownode]);
return ans;
}
void solve(int num)
{
for (int i = 1;i <= n;i++)
for (int j = i + 1;j <= n;j++)if (!used[i][j])
{
int ans = num - Max[i][j] + map[i][j];
if (ans == num)
{
puts("Not Unique!");
return;
}
}
printf("%d\n", num);
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
scanf("%d %d", &n, &m);
int u, v, dis;
memset(map, inf, sizeof(map));
for (int i = 1;i <= m;i++)
{
scanf("%d %d %d", &u, &v, &dis);
map[u][v] = map[v][u] = dis;
}
int ans = prim();
solve(ans);
}
}
D - Is There A Second Way Left?
这题相对于上题来说,有重边。所以用kruskal(个人感觉也能用prim)。。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define ll long long
const int maxn = 105;
const int maxm = 205;
const int inf = 0x3f3f3f3f;
struct Edge
{
int u, v, dis;
bool vis;
Edge(int _u, int _v, int _d) :u(_u), v(_v), dis(_d) { vis = 0; }
Edge(){}
bool operator<(const Edge &b)const
{
return dis < b.dis;
}
}edges[maxm];
int tot;
int n, m;
vector<int>node[maxn];
void addedge(int u, int v, int dis)
{
edges[tot++] = Edge(u, v, dis);
}
int f[maxn];
int find(int a)
{
return a == f[a] ? a : f[a] = find(f[a]);
}
int Max[maxn][maxn];
int Kruskal()
{
sort(edges + 1, edges + tot);
for (int i = 1;i <= n;i++)
f[i] = i, node[i].clear(), node[i].push_back(i);
memset(Max, 0, sizeof(Max));
int ans = 0, tot1 = 0;
for (int i = 1;i < tot;i++)
{
if (tot1 == n - 1)break;
int u = edges[i].u, v = edges[i].v, dis = edges[i].dis;
int fu = find(u), fv = find(v);
if (fu != fv)
{
edges[i].vis = 1;
tot1++;
ans += dis;
f[fv] = fu;
//cout << find(fu) << endl;
int Sizefu = node[fu].size();
int Sizefv = node[fv].size();
for (int j = 0;j < Sizefu;j++)
for (int k = 0;k < Sizefv;k++)
Max[node[fu][j]][node[fv][k]] = Max[node[fv][k]][node[fu][j]] = dis;
int tmp[105];
for (int j = 0;j < Sizefu;j++)
tmp[j] = node[fu][j];
for (int j = 0;j < Sizefv;j++)
node[fu].push_back(node[fv][j]);
for (int j = 0;j < Sizefu;j++)
node[fv].push_back(tmp[j]);
}
}
if (tot1 != n - 1)return -1;
return ans;
}
int solve(int num)
{
int ans = inf;
for (int i = 1;i if (!edges[i].vis)
ans = min(ans, num - Max[edges[i].u][edges[i].v] + edges[i].dis);
if (ans == inf)
return -1;
return ans;
}
int main()
{
int T;
scanf("%d", &T);
int cas = 1;
while (T--)
{
tot = 1;
scanf("%d %d", &n, &m);
int u, v, dis;
for (int i = 1;i <= m;i++)
{
scanf("%d %d %d", &u, &v, &dis);
addedge(u, v, dis);
}
printf("Case #%d : ", cas++);
int ans1 = Kruskal();
if (ans1 == -1)
puts("No way");
else
{
int ans2 = solve(ans1);
if (ans2 == -1)
puts("No second way");
else
printf("%d\n", ans2);
}
}
}
另外一种写法就是先跑kruskal,然后删掉一个边(使边两点并查集一样即可),然后再跑kruskal。这里就不写了。。
接下来就是最小树形图问题,也就是有向图的最小生成树。这里就是朱刘算法的使用。
这里有2个问题需要说明。
1,朱刘算法的模板有两份,一份是矩阵存图,一种是存边。有的时候用其中一份会超时,这时候就得自行判断一下用哪个了。例如下面的G。
2,有向图的最小生成树,有的时候会指定根,那么这个时候直接套就行了。如果没有指定根的话,叫你输出根,这时候就需要建图方法了。首先先记录所有边权和为sum,然后建一个虚点,虚点连向所有点,边权为sum+1,然后跑朱刘算法,如果跑出来的ans>=2*sum+2,就说明图不连通。如何求出的根呢,假设原有边有m条,虚点为s,我们在选边(假设边下标为num)的时候,如果选择到的边有一个点是s的话,那么更新root=num-m。这个也比较好理解。
F - Teen Girl Squad
矩阵存图版本
#include
#include
#include
using namespace std;
const int maxn = 1005;
const int INF = 1e9;
bool vis[maxn];
bool flag[maxn];
int w[maxn][maxn];
int pre[maxn];
int n, m;
void init()//不能少了初始化的内容
{
memset(vis, 0, sizeof(vis));
memset(flag, 0, sizeof(flag));
for (int i = 0; i <= n; i++)
{
w[i][i] = INF;
for (int j = i + 1; j <= n; j++)
w[i][j] = w[j][i] = INF;
}
}
int directed_mst(int u)//u表示根节点
{
int ans = 0;
memset(vis, 0, sizeof(vis));
while (true)
{
//求最短弧集合E
for (int i = 1; i <= n; i++)if (i != u && !flag[i])
{
w[i][i] = INF, pre[i] = i;
for (int j = 1; j <= n; j++)if (!flag[j] && w[j][i]if (pre[i] == i)return -1;//也可以用dfs预处理判断凸的连通
}
//判断E是否有环
int i;
for (i = 1; i <= n; i++)
{
if (i != u && !flag[i])
{
int j = i, cnt = 0;
while (j != u && pre[j] != i && cnt <= n) j = pre[j], ++cnt;
if (j == u || cnt>n) continue; //最后能找到起点(根)或者是走过的点已经超过了n个,表示没有有向环
break;
}
}
if (i>n)
{
for (int i = 1; i <= n; i++)if (i != u && !flag[i]) ans += w[pre[i]][i];
return ans;
}
//有环,进行收缩,把整个环都收缩到一个点i上。
int j = i;
memset(vis, 0, sizeof(vis));
do
{
ans += w[pre[j]][j], j = pre[j], vis[j] = flag[j] = true;//对环内的点标记,并且直接对环的权值进行加和记录,在最后找到最小树形图之后就不用展开收缩点了
} while (j != i);
flag[i] = false; // 环缩成了点i,点i仍然存在
//收缩点的同时,对边权值进行改变
for (int k = 1; k <= n; ++k)if (vis[k]) // 在环中点点
{
for (int j = 1; j <= n; j++)if (!vis[j]) // 不在环中的点
{
if (w[i][j] > w[k][j]) w[i][j] = w[k][j];
if (w[j][k]return ans;
}
int main()
{
int T;
scanf("%d", &T);
int cas = 1;
while (T--)
{
scanf("%d %d", &n, &m);
init();
int u, v,dis;
for (int i = 1;i <= m;i++)
{
scanf("%d %d %d", &u, &v, &dis);
w[u+1][v+1] = min(w[u+1][v+1],dis);
}
printf("Case #%d: ", cas++);
int ans = directed_mst(1);
if (ans == -1)
puts("Possums!");
else
printf("%d\n", ans);
}
return 0;
}
G - Ice_cream’s world II
有的时候用矩阵的会超时,那么这里就用存边版本的。
#include
#include
#include
using namespace std;
#define N 1010
#define INF 0x7f7f7f7f
struct Edge
{
int u,v,w;
} e[N*N];
int cnt;
int in[N];
int vis[N],pre[N],id[N];
int minroot;
void addedge(int u,int v,int w)
{
e[cnt].u=u;
e[cnt].v=v;
e[cnt++].w=w;
}
int Directed_MST(int root,int NV,int NE)
{
int ret = 0;
while(true)
{
///步骤1:找到最小边
for(int i = 0; i < NV; i ++)
in[i] = INF;
memset(pre,-1,sizeof(pre));
for(int i = 0; i < NE; i ++)
{
int u = e[i].u , v = e[i].v;
if(e[i].w < in[v] && u != v)
{
pre[v] = u;
in[v] = e[i].w;
if(u==root) minroot=i;
}
}
for(int i = 0; i < NV; i ++)
{
if(i == root) continue;
if(in[i] == INF) return -1;///除了根节点以外有点没有入边,则根无法到达他
}
int cntnode = 0;
memset(id,-1,sizeof(id));
memset(vis,-1,sizeof(vis));
///找环
in[root] = 0;
for(int i = 0; i < NV; i ++) ///标记每个环,编号
{
ret += in[i];
int v = i;
while(vis[v] != i && id[v] == -1 && v != root)
{
vis[v] = i;
v = pre[v];
}
if(v != root && id[v] == -1)
{
for(int u = pre[v]; u != v; u = pre[u])
{
id[u] = cntnode;
}
id[v] = cntnode ++;
}
}
if(cntnode == 0) break;//无环
for(int i = 0; i < NV; i ++)
if(id[i] == -1)
id[i] = cntnode ++;
///步骤3:缩点,重新标记
for(int i = 0; i < NE; i ++)
{
int u=e[i].u;
int v = e[i].v;
e[i].u = id[u];
e[i].v = id[v];
if(e[i].u != e[i].v) e[i].w -= in[v];
}
NV = cntnode;
root = id[root];
}
return ret;///最小树形图的长度
}
int main()
{
int n,m,sum;
int u,v,w;
while(scanf("%d %d",&n,&m)!=EOF)
{
cnt=0;sum=0;
for(int i=0; i"%d %d %d",&u,&v,&w);
addedge(u+1,v+1,w);
sum+=w;
}
sum++;
for(int i=1; i<=n; i++)
addedge(0,i,sum);
int ans=Directed_MST(0,n+1,cnt);
if(ans==-1||ans>=2*sum)
printf("impossible\n\n");
else
printf("%d %d\n\n",ans-sum,minroot-m);
}
return 0;
}
接下来就是生成树计数问题,做这个问题,首先你得要有一个矩阵模板。
这里说明一下矩阵的一个问题。
有的时候需要模一个数,那么矩阵求行列式就只能用辗转相除法,否则就能用高精度。
生成树计数目前碰到的一个问题,就是限制边权最小的生成树计数,也就是最小生成树计数,这个有点难,稍后会解释。
L - Lightning
随便贴一个题来展示博主的模板。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
typedef long double ld;
const ld eps = 1e-10;
const int maxn = 305;
const int mod = 10007;
int sgn(ld x)
{
if (fabs(x) < eps)return 0;
else if (x > 0)return 1;
return -1;
}
struct M
{
int n, m;
ll a[maxn][maxn];
M(int _n = 0) { n = m = _n;memset(a, 0, sizeof(a)); }
M(int _n, int _m) { n = _n, m = _m;memset(a, 0, sizeof(a)); }
void mem(int _n = 0) { n = m = _n, memset(a, 0, sizeof(a)); }
void mem(int _n, int _m) { n = _n, m = _m;memset(a, 0, sizeof(a)); }
void pri()
{
for (int i = 1;i <= n;i++)
{
for (int j = 1;j <= m;j++)cout << a[i][j] << ' ';
cout << endl;
}
}
friend M operator*(M a, M b)
{
M c;
for (int k = 1;k <= a.m;k++)
for (int i = 1;i <= a.n;i++)
for (int j = 1;j <= b.m;j++)
c.a[i][j] += a.a[i][k] * b.a[k][j];
return c;
}
friend M operator-(M a, M b)
{
for (int i = 1;i <= a.n;i++)
for (int j = 1;j <= a.m;j++)
a.a[i][j] -= b.a[i][j];
return a;
}
void make_I(int _n)
{
n = m = _n;
memset(a, 0, sizeof(a));
for (int i = 1;i <= n;i++)a[i][i] = 1;
}
//行列式高精度
long double mat[maxn][maxn], tmp[maxn];
long double det()
{
long double ans = 1;
for (int i = 1;i <= n;i++) for (int j = 1;j <= m;j++) mat[i][j] = a[i][j];
for (int i = 1;i <= n;i++)
{
int pos = i;
while (fabs(mat[pos][i])if (fabs(mat[pos][i])return 0;
if (pos^i)
{
copy(mat[pos] + 1, mat[pos] + 1 + m + 1, tmp + 1);
copy(mat[i] + 1, mat[i] + 1 + m + 1, mat[pos] + 1);
copy(tmp + 1, tmp + 1 + m + 1, mat[i] + 1);
}
ans *= mat[i][i];
for (int j = i + 1;j <= n;j++)
{
long double p = mat[j][i] / mat[i][i];
for (int k = i;k <= m;k++) mat[j][k] -= mat[i][k] * p;
}
}
return ans;
}
//行列式辗转相除法
ll det(ll mod)
{
ll ret = 1;
for (int i = 1;i <= n;i++)
{
if (a[i][i] < 0)
{
ret = -ret;
for (int k = i;k <= n;k++)a[i][k] = -a[i][k];
}
for (int j = i + 1;j <= n;j++)
{
for (int k = i;k <= n;k++)a[i][k] %= mod, a[j][k] %= mod;
while (a[j][i])
{
if (a[j][i] < 0)
{
ret = -ret;
for (int k = i;k <= n;k++)a[j][k] = -a[j][k];
}
ll t = a[i][i] / a[j][i];
for (int k = i;k <= n;k++)a[i][k] = (a[i][k] - t*a[j][k]) % mod;
for (int k = i;k <= n;k++)swap(a[i][k], a[j][k]);
ret = -ret;
}
}
if (a[i][i] == 0)return 0;
ret = ret*a[i][i] % mod;
}
return (ret + mod) % mod;
}
}A, C, D;
struct node
{
int x, y;
node(int _x,int _y):x(_x),y(_y){}
node(){}
}nodes[maxn];
double map[maxn][maxn];
int N, R;
double dist(int a, int b)
{
double x = nodes[a].x - nodes[b].x;
double y = nodes[a].y - nodes[b].y;
return sqrt(x*x + y*y);
}
vector<int>V[maxn];
bool check(int a, int b)
{
for(auto it:V[a])if(it!=b)
if (!sgn(map[a][it] + map[it][b] - map[a][b]))
return 0;
return 1;
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
scanf("%d %d", &N, &R);
for (int i = 1;i <= N;i++)V[i].clear();
A.mem(N);
C.mem(N);
D.mem(N);
int x, y;
for (int i = 1;i <= N;i++)
{
scanf("%d %d", &x, &y);
nodes[i] = node(x, y);
}
for(int i=1;i<=N;i++)
for (int j = i+1;j <= N;j++)if (i != j)
{
double tmp = dist(i, j);
map[i][j] = map[j][i] = tmp;
if (tmp <= R)
V[i].push_back(j), V[j].push_back(i);
}
for(int i=1;i<=N;i++)
for (int j = i + 1;j <= N;j++)if (map[i][j] <= R)
{
if (check(i, j))
{
A.a[i][j] = A.a[j][i] = 1;
D.a[i][i]++;
D.a[j][j]++;
}
}
for (int i = 1;i <= N;i++)
for (int j = 1;j <= N;j++)
C.a[i][j] = D.a[i][j] - A.a[i][j];
//C.pri();
C.n--;C.m--;
int ans = C.det(mod);
printf("%d\n", ans == 0 ? -1 : ans);
}
}
M - Minimum Spanning Tree
如何计算最小生成树个数呢?
首先我们回顾一下kruskal算法。
kruskal算法首先将边按照边权排序,假设边权先有一堆c1,然后c2,然后c3…(c1< c2< c3).
我们首先会算c1的结果,而计算c2时,前面的结果实际和现在算c2是互不干扰的。也就是说,我们在计算最小生成树时,可以把它分成若干个阶段。在算c1阶段时,我们可以算它的生成树个数,然后再算c2时,再算它的生成树个数,然后相乘就是答案了。
知道思路了,再说一个比较混的点。
设fa[maxn]为目前的连通状态,ka[maxn]为更新后的连通状态。在找边的时候,不能更新fa,而只能更新ka,在计算完生成树后,再更新fa。
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
typedef long double ld;
const ld eps = 1e-10;
const int maxn = 105;
const int maxm = 1005;
const int mod = 1e9;
int sgn(ld x)
{
if (fabs(x) < eps)return 0;
else if (x > 0)return 1;
return -1;
}
struct M
{
int n, m;
ll a[maxn][maxn];
M(int _n = 0) { n = m = _n;memset(a, 0, sizeof(a)); }
M(int _n, int _m) { n = _n, m = _m;memset(a, 0, sizeof(a)); }
void mem(int _n = 0) { n = m = _n, memset(a, 0, sizeof(a)); }
void mem(int _n, int _m) { n = _n, m = _m;memset(a, 0, sizeof(a)); }
void pri()
{
for (int i = 1;i <= n;i++)
{
for (int j = 1;j <= m;j++)cout << a[i][j] << ' ';
cout << endl;
}
}
friend M operator*(M a, M b)
{
M c;
for (int k = 1;k <= a.m;k++)
for (int i = 1;i <= a.n;i++)
for (int j = 1;j <= b.m;j++)
c.a[i][j] += a.a[i][k] * b.a[k][j];
return c;
}
friend M operator-(M a, M b)
{
for (int i = 1;i <= a.n;i++)
for (int j = 1;j <= a.m;j++)
a.a[i][j] -= b.a[i][j];
return a;
}
void make_I(int _n)
{
n = m = _n;
memset(a, 0, sizeof(a));
for (int i = 1;i <= n;i++)a[i][i] = 1;
}
//行列式高精度
long double mat[maxn][maxn], tmp[maxn];
long double det()
{
long double ans = 1;
for (int i = 1;i <= n;i++) for (int j = 1;j <= m;j++) mat[i][j] = a[i][j];
for (int i = 1;i <= n;i++)
{
int pos = i;
while (fabs(mat[pos][i])if (fabs(mat[pos][i])return 0;
if (pos^i)
{
copy(mat[pos] + 1, mat[pos] + 1 + m + 1, tmp + 1);
copy(mat[i] + 1, mat[i] + 1 + m + 1, mat[pos] + 1);
copy(tmp + 1, tmp + 1 + m + 1, mat[i] + 1);
}
ans *= mat[i][i];
for (int j = i + 1;j <= n;j++)
{
long double p = mat[j][i] / mat[i][i];
for (int k = i;k <= m;k++) mat[j][k] -= mat[i][k] * p;
}
}
return ans;
}
//行列式辗转相除法
ll det(ll mod)
{
ll ret = 1;
for (int i = 1;i <= n;i++)
{
if (a[i][i] < 0)
{
ret = -ret;
for (int k = i;k <= n;k++)a[i][k] = -a[i][k];
}
for (int j = i + 1;j <= n;j++)
{
for (int k = i;k <= n;k++)a[i][k] %= mod, a[j][k] %= mod;
while (a[j][i])
{
if (a[j][i] < 0)
{
ret = -ret;
for (int k = i;k <= n;k++)a[j][k] = -a[j][k];
}
ll t = a[i][i] / a[j][i];
for (int k = i;k <= n;k++)a[i][k] = (a[i][k] - t*a[j][k]) % mod;
for (int k = i;k <= n;k++)swap(a[i][k], a[j][k]);
ret = -ret;
}
}
if (a[i][i] == 0)return 0;
ret = ret*a[i][i] % mod;
}
return (ret + mod) % mod;
}
}A, C;
struct Edge
{
int u, v, dist;
Edge(int _u, int _v, int _d) :u(_u), v(_v), dist(_d) {}
Edge() {}
bool operator<(const Edge &b)const
{
return dist < b.dist;
}
}edges[maxm];
int n, m, p;
int find(int a, int *f) { return a == f[a] ? a : f[a] = find(f[a], f); }
int f[maxn], ka[maxn];
bool vis[maxn];
vector<int>V[maxn];
ll matrix_tree()
{
for (int i = 1;i <= n;i++)if (vis[i])
V[find(i, ka)].push_back(i);
memset(vis, 0, sizeof(vis));
ll ans = 1;
for (int i = 1;i <= n;i++)if (V[i].size())
{
int Size = V[i].size();
C.mem(Size);
for (int j = 0;jfor (int k = j + 1;k < Size;k++)
{
int u = V[i][j], v = V[i][k];
C.a[j + 1][k + 1] = 0 - A.a[u][v];
C.a[k + 1][j + 1] = 0 - A.a[v][u];
C.a[j + 1][j + 1] += A.a[u][v];
C.a[k + 1][k + 1] += A.a[u][v];
}
C.n--, C.m--;
ans = ans*C.det(p) % p;
V[i].clear();
}
for (int i = 1;i <= n;i++)
f[find(i, f)] = find(i, ka);
return ans;
}
void solve()
{
sort(edges + 1, edges + 1 + m);
for (int i = 1;i <= n;i++)f[i] = ka[i] = i;
int now = edges[1].dist;
int u, v, fu, fv;
ll ans = 1;
for (int i = 1;i <= m;i++)
{
u = edges[i].u, v = edges[i].v;
fu = find(u, f), fv = find(v, f);
if (fu != fv)
{
ka[find(fu, ka)] = find(fv, ka);
A.a[fu][fv]++;
A.a[fv][fu]++;
vis[fu] = 1, vis[fv] = 1;
}
if (i == m || now != edges[i + 1].dist)
{
ans = ans*matrix_tree() % p;
now = edges[i + 1].dist;
}
}
for (int i = 2;i <= n;i++)if (ka[i] != ka[i - 1])ans = 0;
printf("%lld\n", ans%p);
}
int main()
{
while (~scanf("%d %d %d", &n, &m, &p) && n)
{
A.mem(n);
int u, v, dis;
for (int i = 1;i <= m;i++)
{
scanf("%d %d %d", &u, &v, &dis);
edges[i] = Edge(u, v, dis);
}
solve();
}
}
这题有点毒瘤的地方是,如果我不在printf那里模p的话,竟然会wa。至今不知道原因。。
之后会补充对朱刘算法和矩阵树的理解。