1月18日 LCA专项训练
B.Minimum spanning tree for each edge
C.Misha, Grisha and Underground
题意:给你一颗基环树(有n条边,n个点的连通图),有Q个询问,求u,v之间的距离最小是多少
思路:对于一棵基环树而言,只要去掉其环上任意一条边(a , b),其边权为w ,剩下的边就可以构成一棵树
对于 u,v 之间的最小距离 , 有可能由去掉的边(a , b)构成 ,也有可能不需要边(a , b)
不需要L的情况
ans = dis(u , v)
需要L的情况
ans = min(dis(u , a) + dis(v , b) + w , dis(v , a) + dis(u , b) ,w)
两种情况取min即可。
dfs序+RMQ做法(这个做法虽然有点麻烦,但是查询时O(1)的)
#include<cstdio> #include<string.h> #include<algorithm> #include<cmath> #include<iostream> #include<vector> #include<queue> #include<set> #include<map> #include<stack> #include<cctype> #define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) #define mem(a,x) memset(a,x,sizeof(a)) #define lson rt<<1,l,mid #define rson rt<<1|1,mid + 1,r #define P pair<int,int> #define ull unsigned long long using namespace std; typedef long long ll; const int maxn = 1e5 + 10; const ll mod = 1e9 + 7; const int inf = 0x3f3f3f3f; const long long INF = 0x3f3f3f3f3f3f3f3f; const double eps = 1e-7; int n; int f[maxn]; int qx, qy, qv, cnt, num, si; struct node { int y, v; node(int a, int b) { y = a; v = b; } }; vector<node>G[maxn]; int dp[20][maxn * 2], dis[maxn], vis[maxn], pos[maxn], res[maxn]; // res 存储欧拉序列 pos 存储每个节点第一次出现的位置 void init() { qx = qy = qv = 0; cnt = 0; num = 0; si = 0; mem(dp, 0); mem(dis, 0); mem(vis, 0); mem(res, 0); mem(pos, 0); for (int i = 0; i < maxn; ++i) G[i].clear(); } int find(int x) { return x == f[x] ? f[x] : f[x] = find(f[x]); } void bset(int x, int y) { int fx = find(x), fy = find(y); f[fx] = fy; } void dfs(int u, int dist) { vis[u] = 1; dis[u] = dist; pos[u] = cnt; res[si] = u; dp[0][cnt++] = si++; for (int i = 0; i < G[u].size(); ++i) { int j = G[u][i].y; if (!vis[j]) { dfs(j, dist + G[u][i].v); dp[0][cnt++] = dp[0][pos[u]]; } } } void RMQ() { for (int i = 1; (1 << i) <= n; ++i) { for (int j = n - 1; j >= 0; --j) { int k = (1 << (i - 1)); dp[i][j] = dp[i - 1][j]; if (k + j < n) { dp[i][j] = min(dp[i][j], dp[i - 1][j + k]); } } } } int cal(int i, int j) { if (i < j) swap(i, j); int k = 0; while ((1 << k) <= (i - j + 1)) ++k; --k; k = min(dp[k][j], dp[k][i - (1 << k) + 1]); return res[k]; } int Dis(int u, int v) { int k = cal(pos[u], pos[v]); return dis[u] + dis[v] - dis[k] * 2; } int main() { while (scanf("%d", &n) != EOF) { if (n == 0) break; init(); for (int i = 0; i <= n; ++i) f[i] = i; for (int i = 1; i <= n; ++i) { int x, y, v; scanf("%d %d %d", &x, &y, &v); x++, y++; int fx = find(x), fy = find(y); if (fx == fy) { qx = x, qy = y, qv = v; continue; } bset(x, y); G[x].push_back(node(y, v)); G[y].push_back(node(x, v)); } for (int i = 1; i <= n; ++i) { if (!vis[i]) dfs(i, 0); } n = n * 2 - 1; RMQ(); int q; scanf("%d", &q); while (q--) { int x, y; scanf("%d %d", &x, &y); x++, y++; int ans = Dis(x, y); ans = min(ans, Dis(x, qx) + Dis(y, qy) + qv); ans = min(ans, Dis(x, qy) + Dis(y, qx) + qv); printf("%d\n", ans); } } return 0; } /* 7 0 1 2 0 2 3 1 3 2 2 3 8 2 4 3 3 5 1 1 6 7 3 4 5 0 6 1 2 0 */
题意:给你一颗由n个点构成的树,在给你m条链,让你选择一些不相交的链,使得权值最大
思路:用dp[i]记录以i为根节点的子树的权值最大值 , sum[i] 表示点 i 的所有儿子的dp值的和
考虑到动态规划的无后效性,因此给我们的链我们尽在lca(u , v)处考虑拿或不拿(u , v 为其中一条链的端点)
这一点的状态dp[i] ,仅有两种状况
不取 lca(u , v) = i 的这条链
dp[i] = sum[i]
取 lca(u , v) = i 的这条链
dp[i] = ( sum[i] + ∑ (sum[vi] - dp[vi]) + w ) vi 为链上的每一个节点
因为需要取 lca(u , v) = i 的这条链 因此 vi 点不能取其他的链, 因为题目要求链之间不相交
为了快速计算 ∑ (sum[vi] - dp[vi]) ,用 dfs序 和 树状数组 来维护 ∑ (sum[vi] - dp[vi]) 的前缀和
当我们求 ∑ (sum[vi] - dp[vi]) 只需要在 i 节点加入 树状数组前 查询 区间[1 , u] + 区间[1 , v] 的和
#pragma comment(linker,"/STACK:1024000000,1024000000") #include<algorithm> #include<iostream> #include<cstring> #include<cstdio> #include<vector> using namespace std; const int maxn = 202020; const int maxb = 22; struct Node { int u, v, w; Node(int u, int v, int w) :u(u), v(v), w(w) {} }; int n, m; vector<Node> que[maxn]; vector<int> G[maxn]; int lca[maxn][maxb], in[maxn], out[maxn], dep[maxn], dfs_cnt; int sumv[maxn];// 树状数组 int dp[maxn], sum[maxn]; //计算dfs序,in,out;预处理每个顶点的祖先lca[i][j],表示i上面第2^j个祖先,lca[i][0]表示父亲 void dfs(int u, int fa, int d) { in[u] = ++dfs_cnt; // 获取每个点进入的时间 lca[u][0] = fa; dep[u] = d; for (int j = 1; j < maxb; j++) { int f = lca[u][j - 1]; lca[u][j] = lca[f][j - 1]; } for (int i = 0; i < G[u].size(); i++) { int v = G[u][i]; if (v == fa) continue; dfs(v, u, d + 1); } out[u] = ++dfs_cnt; // 获取每个点出去的时间 } // 倍增法在线求lca ,o(n*logn)预处理+o(logn)询问 int Lca(int u, int v) { if (dep[u] < dep[v]) swap(u, v); //二进制倍增法,u,v提到相同高度 for (int i = maxb - 1; i >= 0; i--) { if (dep[lca[u][i]] >= dep[v]) u = lca[u][i]; } //当lca为u或者为v的时候 if (u == v) return u; //lca不是u也不是v的情况 //一起往上提 for (int i = maxb - 1; i >= 0; i--) { if (lca[u][i] != lca[v][i]) { u = lca[u][i]; v = lca[v][i]; } } return lca[u][0]; } //因为需要求出区间[in[u] , out[u]]上的(sum[i] - dp[i])的和 , 用树状数组维护 int get_sum(int x) { int ret = 0; while (x > 0) { ret += sumv[x]; x -= x & (-x); } return ret; } void add(int x, int v) { while (x < maxn) { sumv[x] += v; x += x & (-x); } } //树形dp(用到dfs序和树状数组来快速计算链) void solve(int u, int fa) { for (int i = 0; i < G[u].size(); i++) { int v = G[u][i]; if (v == fa) continue; solve(v, u); sum[u] += dp[v]; } dp[u] = sum[u]; // 先将dp[u]处理为不选择以u为lca的链 for (int i = 0; i < que[u].size(); i++) { Node nd = que[u][i]; //get_sum(in[nd.u])处理的是lca(u,v)到u点这条路径的所有顶点 //get_sum(in[nd.v])处理的是lca(u,v)到v点这条路径的所有顶点 dp[u] = max(dp[u], sum[u] + get_sum(in[nd.u]) + get_sum(in[nd.v]) + nd.w); } add(in[u], sum[u] - dp[u]); add(out[u], dp[u] - sum[u]); } void init() { dfs_cnt = 0; for (int i = 1; i <= n; i++) G[i].clear(); for (int i = 1; i <= n; i++) que[i].clear(); memset(lca, 0, sizeof(lca)); memset(sumv, 0, sizeof(sumv)); memset(sum, 0, sizeof(sum)); memset(dp, 0, sizeof(dp)); } int main() { int tc; scanf("%d", &tc); while (tc--) { scanf("%d%d", &n, &m); init(); for (int i = 1; i < n; i++) { int u, v; scanf("%d%d", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs(1, 0, 1); while (m--) { int u, v, w; scanf("%d%d%d", &u, &v, &w); //每条链在Lca的位置上处理,这样符合dp的无后效性 que[Lca(u, v)].push_back(Node(u, v, w)); } solve(1, 0); printf("%d\n", dp[1]); } for (int i = 1; i <= n; ++i) { printf("%d ", dp[i]); } printf("\n"); for (int i = 1; i <= n; ++i) { printf("%d ", sum[i]); } printf("\n"); return 0; }