一、经典dp问题
1. 背包
2. 最长公共子序列(LCS)
(1)hdu 1159 Common Subsquences
题意:求两个字符串的公共子序列
思路:dp求公共子序列,a[i] = b[j]时,dp[i][j] = dp[i - 1][j - 1] + 1,否则dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])。
注意:两个字符串之间可能有多个空格。dp递推时关于i = 0 / j = 0的处理。
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
char a[1005], b[1005];
int dp[1005][1005];
int main(void)
{
char ch;
int pb = 0;
while(scanf("%s", &a) != EOF)
{
scanf("%c", &ch);
while(ch == ' ')
scanf("%c", &ch);
pb = 0;
while(ch != '\n')
{
if(ch != ' ')
b[pb ++] = ch;
scanf("%c", &ch);
}
b[pb] = '\0';
int la = strlen(a), lb = strlen(b);
memset(dp, 0, sizeof(dp));
for(int i = 0; i < la; ++ i)
{
for(int j = 0; j < lb; ++ j)
{
if(a[i] == b[j])
{
if(i == 0 || j == 0)
dp[i][j] = 1;
else
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else
{
if(i == 0 && j == 0)
dp[i][j] = 0;
else if(i == 0)
dp[i][j] = dp[i][j - 1];
else if(j == 0)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
printf("%d\n", dp[la - 1][lb - 1]);
}
return 0;
}
3. 最长递增子序列(LIS)
(1)hdu 1087 Super jumping! jumping! jumping!
题意:给定一个正整数序列,求所有递增子序列中和最大的那个。
思路:设sum[i]表示i之前所有递增子序列中的最大和。那么sum[i] = max{sum[j] + a[i], a[i] > a[j]}。求出所有sum后,再求sum中的最大值即可。
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int a[1005], sum[1005];
int main(void)
{
int n;
while(scanf("%d", &n) != EOF && n != 0)
{
for(int i = 1; i <= n; ++ i)
scanf("%d", &a[i]);
memset(sum, 0, sizeof(sum));
for(int i = 1; i <= n; ++ i)
sum[i] = a[i];
for(int i = 1; i <= n; ++ i)
{
for(int j = 1; j < i; ++ j)
{
if(a[i] > a[j])
sum[i] = max(sum[i], sum[j] + a[i]);
}
}
int ans = 0;
for(int i = 1; i <= n; ++ i)
ans = max(ans, sum[i]);
printf("%d\n", ans);
}
return 0;
}
(2)hdu 1003 Max Sum
题意:给定一个整数序列,求具有最大和的子串(必须是连续的)。
思路:设dp[i]代表以a[i]结尾的子串的最大和,那么dp[i] = max(dp[i - 1] + a[i], a[i]),由于dp[i]表示的是以a[i]结尾的所有子串中的最大和,因此dp[i]只可能是dp[i - 1] + a[i](当前数连着前面的算上)或者a[i](从当前开始一个新的子串)。
注意:该题dp数组含义的设置非常重要,保证其无后效性。
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int a[100005], dp[100005], l[100005];
int main(void)
{
int tcase, n;
scanf("%d", &tcase);
for(int tx = 1; tx <= tcase; ++ tx)
{
scanf("%d", &n);
for(int i = 1; i <= n; ++ i)
scanf("%d", &a[i]);
dp[1] = a[1];
l[1] = 1;
for(int i = 2; i <= n; ++ i)
{
if(dp[i - 1] + a[i] >= a[i])
{
dp[i] = dp[i - 1] + a[i];
l[i] = l[i - 1];
}
else
{
dp[i] = a[i];
l[i] = i;
}
}
int ans = -0x3f3f3f3f, anx;
for(int i = 1; i <= n; ++ i)
{
if(dp[i] > ans)
{
ans = dp[i];
anx = i;
}
}
printf("Case %d:\n", tx);
printf("%d %d %d\n", ans, l[anx], anx);
if(tx < tcase)
printf("\n");
}
return 0;
}
二、区间dp
参考博客:博客地址
1. nyist 737 石子合并 题目链接
题意:n堆石子,每次只能合并相邻的两堆,合并的代价为两堆石子的总和,求将所有石子合并成一堆所需的最小代价。
思路:dp[i][j]表示合并i到j堆石子需要的最小代价,dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]),其中w[i][j]代表第i到j堆的石子之和。
注意:合并代价是两堆石子合并的总代价,因此dp[i][i] = 0。区间dp进行递推时,最外层枚举区间长度,然后内层枚举区间起点,最内层枚举区间的分割点。
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int a[205], dp[205][205], ans[205];
int main(void)
{
int n;
while(scanf("%d", &n) != EOF)
{
ans[0] = 0;
for(int i = 1; i <= n; ++ i)
{
scanf("%d", &a[i]);
ans[i] = ans[i - 1] + a[i];
}
for(int i = 1; i <= n; ++ i)
{
for(int j = 1; j <= n; ++ j)
{
if(i == j)
dp[i][j] = 0;
else
dp[i][j] = 0x3f3f3f3f;
}
}
for(int i = 2; i <= n; ++ i)
{
for(int j = 1; j <= n; ++ j)
{
int r = j + i - 1;
if(r > n) break;
for(int k = j; k < r; ++ k)
{
dp[j][r] = min(dp[j][r], dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1]);
}
}
}
printf("%d\n", dp[1][n]);
}
return 0;
}
四边形优化 —— 结论记住 参考博客链接: 参考博客 O(n^3)优化至O(n^2)
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int a[205], dp[205][205], ans[205], s[205][205];
int main(void)
{
int n;
while(scanf("%d", &n) != EOF)
{
ans[0] = 0;
for(int i = 1; i <= n; ++ i)
{
scanf("%d", &a[i]);
ans[i] = ans[i - 1] + a[i];
}
for(int i = 1; i <= n; ++ i)
{
for(int j = 1; j <= n; ++ j)
{
if(i == j)
dp[i][j] = 0;
else
dp[i][j] = 0x3f3f3f3f;
}
}
for(int i = 1; i <= n; ++ i)
s[i][i] = i;
for(int i = 2; i <= n; ++ i)
{
for(int j = 1; j <= n; ++ j)
{
int r = j + i - 1;
if(r > n) break;
for(int k = s[j][r - 1]; k <= s[j + 1][r]; ++ k)
{
if(dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1] < dp[j][r])
{
dp[j][r] = dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1];
s[j][r] = k;
}
}
}
}
printf("%d\n", dp[1][n]);
}
return 0;
}
2. hdu 3506 Monkey Party
题意:问题可转化为n堆石子排成1个圈,将其合并成1堆,求合并的最小代价。
思路:原来的石子合并是n堆排成一条线,现在是一个圈。将前n - 1堆石子加到n堆石子的后面,构成一个2 * n - 1堆石子的链,求合成一堆的最小值,就是求dp[i][i + n - 1], i = 1,2,3.... n的最小值,因为对于一个圈来讲,展开以后,每一个点都可以看作是对应线段的左端点。问题转化好之后,可以使用与上一道题相同的方法求解,要用四边形优化。
注意:(1)环展成线以后,数组大小要翻倍。(2)在dp递推时,枚举顶点的时候,要从1枚举到2*n-1,而不是只枚举到n,因为对于不从顶点1展开的环来讲,它在整个2*n-1的线段上会覆盖从i到i+n-1这段线段,其中有超出n的部分,因此分割点可能会超过n,所以起点枚举必须到2*n-1。
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int a[2005], dp[2005][2005], ans[2005], s[2005][2005];
int main(void)
{
int n;
while(scanf("%d", &n) != EOF)
{
ans[0] = 0;
for(int i = 1; i <= n; ++ i)
{
scanf("%d", &a[i]);
ans[i] = ans[i - 1] + a[i];
}
for(int i = n + 1; i <= 2 * n - 1; ++ i)
{
a[i] = a[i - n];
ans[i] = ans[i - 1] + a[i];
}
for(int i = 1; i <= 2 * n - 1; ++ i)
{
for(int j = 1; j <= 2 * n - 1; ++ j)
{
if(i == j)
{
dp[i][j] = 0;
s[i][j] = i;
}
else
dp[i][j] = 0x3f3f3f3f;
}
}
for(int i = 2; i <= n; ++ i)
{
for(int j = 1; j <= 2 * n - 1; ++ j)
{
int r = j + i - 1;
if(r > 2 * n - 1) break;
for(int k = s[j][r - 1]; k <= s[j + 1][r]; ++ k)
{
if(dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1] < dp[j][r])
{
dp[j][r] = dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1];
s[j][r] = k;
}
}
}
}
int ans = 0x3f3f3f3f;
for(int i = 1; i <= n; ++ i)
ans = min(ans, dp[i][i + n - 1]);
printf("%d\n", ans);
}
return 0;
}
3. poj 2955 Brackets
题意:已知'()'和'[]'是匹配的括号形式,求给定字符串中最大的匹配序列长度。
思路:设dp[i][j]代表区间[i,j]上的最大匹配长度。如果a[i]和a[r]是匹配的,如果[i,r]上的最大匹配长度子序列中i和r恰好匹配,那么dp[i][r] = dp[i + 1][r - 1] + 2,然后再枚举区间[i,r]上的每一个分割点,取最大值,即dp[i][r] = max(dp[i][r], dp[i][k] + dp[k + 1][r])。
注意:不论a[i]和a[r]是否恰好匹配,对于每一个区间[i, r],枚举区间中的每一个分割点并取最大值都是必须的。因为,如果a[i]和a[r]匹配,dp[i][r] = dp[i + 1][r - 1] + 2代表最大长度的匹配子序列中a[i]和a[r]匹配,但是这种情况下有可能最大长度的匹配子序列并不是a[i]和a[r]进行匹配,而是他们分别与区间中的其他位置匹配,然后取得了更大的长度。比如序列( ) [ ] ( ),虽然第一个和最后一个字符是匹配的,但显然在最大长度的匹配子序列里面,他们并不是相互匹配的那一对,而是分别和自己相邻的括号进行匹配,这样取得的才是最大的匹配长度。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
char a[105];
int dp[105][105];
int main(void)
{
int la = 0;
while(scanf("%s", a) != EOF && strcmp(a, "end") != 0)
{
la = strlen(a);
memset(dp, 0, sizeof(dp));
for(int len = 2; len <= la; ++ len)
{
for(int i = 0; i < la; ++ i)
{
int r = i + len - 1;
if(r >= la)
break;
if((a[i] == '(' && a[r] == ')') || a[i] == '[' && a[r] == ']')
{
if(r - i >= 2)
dp[i][r] = dp[i + 1][r - 1] + 2;
else
dp[i][r] = 2;
}
for(int k = i; k < r; ++ k)
dp[i][r] = max(dp[i][r], dp[i][k] + dp[k + 1][r]);
}
}
printf("%d\n", dp[0][la - 1]);
}
return 0;
}
4. 整数划分问题
给定整数n,m,在n中添加m个乘号将n分成m个部分,求可以得到的最大乘积。
思路:设dp[i][j]代表第i位之前插入了j个乘号所能得到的最大乘积。接下来,我们要枚举乘号添加的位置,dp[i][j] = max(dp[i][j], dp[k][j - 1] * num[k + 1][i]),其中num[i][j]代表从i位到j位所代表的的数值。
具体代码实现可见如下链接 整数划分部分内容
5. 凸多边形三角划分问题
给定整数n,每个顶点都有一个权值,将凸n边形划分为n-2个三角形,求所有三角形各顶点乘积的和的最小值。
思路:设dp[i][j]表示从顶点i到顶点j构成的凸多边形对应的乘积和的最小值。枚举凸多边形的分割点k,则有dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[j] * a[k])。
具体代码实现可见如下链接 凸多边形三角划分部分
6. hdu 2513 Cake Slicing
题意:n*m的矩形蛋糕,某些单元内放置樱桃,每次切割蛋糕时必须沿水平或垂直方向切割并且切到底将蛋糕分成两部分,要求经过若干次切割后每部分蛋糕中只含有一个樱桃,求各次切割的最小切割长度之和。
思路:设dp[a][b][c][d]代表以(a,b)为左下角(c,d)为右上角的蛋糕切割成各部分只含有一个樱桃的最小切割长度和,接下来枚举切割的位置,可能沿水平方向切割,可能沿垂直方向切割,取所有情况中的最小的那个。这里采用深搜的方法来写dp即记忆化搜索。
记忆化搜索碰到已算过的dp值则直接返回,碰到没计算过的才会继续向下递归计算,相当于dp的递归形式。
注意:这里注意每个坐标代表的是该单元的中心位置而不是某一单元的某顶点坐标,如(1,1)就代表最左下那个单元格。注意枚举切割位置时的上下界。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int dp[25][25][25][25], mark[25][25];
int dfs(int a, int b, int c, int d)
{
if(dp[a][b][c][d] != -1)
return dp[a][b][c][d];
int ans = 0;
for(int i = a; i <= c; ++ i)
{
for(int j = b; j <= d; ++ j)
{
if(mark[i][j] == 1)
ans ++;
}
}
if(ans <= 1)
return dp[a][b][c][d] = 0;
int mins = 0x3f3f3f3f;
for(int i = b; i < d; ++ i)
mins = min(mins, dfs(a, b, c, i) + dfs(a, i + 1, c, d) + c - a + 1);
for(int i = a; i < c; ++ i)
mins = min(mins, dfs(a, b, i, d) + dfs(i + 1, b, c, d) + d - b + 1);
return dp[a][b][c][d] = mins;
}
int main(void)
{
int n, m, k, a, b, tcase = 1;
while(scanf("%d%d%d", &n, &m, &k) != EOF)
{
memset(mark, 0, sizeof(mark));
memset(dp, -1, sizeof(dp)); //memset仅能初始化为0或者-1
for(int i = 1; i <= k; ++ i)
{
scanf("%d%d", &a, &b);
mark[a][b] = 1;
}
int ans = dfs(1, 1, n, n);
printf("Case %d: %d\n", tcase, ans);
tcase ++;
}
return 0;
}
7. hdu 2476 String Painter
题意:给定两个字符串A, B,每次操作可将字符串A某一区间全部改成同一个字符,求至少经过多少次操作可以使字符串A变为字符串B。
思路:该题直接考虑把A串直接转换成B串,不好想。先用dp[i][j]代表把一个空白串转换成B串所需要的最小步数,那么dp[i][j]初始时为dp[i + 1][j] + 1,意思代表b[i]单独转换一次,然后加上从i + 1到j之间的转换次数。接下来考虑,在i + 1到j之间,如果某一个位置有b[k] = b[i]的话,那么在转化的过程中,b[i]可以在包含b[k]的某一段刷新时被刷新到而不需单独自己刷新一次,这个时候dp[i][j] = min(dp[i][j], dp[i + 1][k] +dp[k + 1][j])。
接下来用ans[i]代表从0到i由A刷新成B串需要的最小步数,初始时ans[i] = dp[0][i],如果有a[i] = b[i],那么这个位置不用刷新,有ans[i] = ans[i - 1],否则,枚举所有的分割点,ans[i] = min(ans[i], ans[j] + dp[j + 1][i])。
注意:在dp[i][j]递推时,枚举k的那一步,k要枚举到边界r,也就是从i + 1到j整个变成一段。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int dp[105][105], ans[105];
char a[105], b[105];
void solve(int l)
{
memset(dp, 0, sizeof(dp));
for(int len = 1; len <= l; ++ len)
{
for(int i = 0; i < l; ++ i)
{
int r = i + len - 1;
if(r >= l) break;
dp[i][r] = dp[i + 1][r] + 1;
for(int k = i + 1; k <= r; ++ k)
{
if(b[i] == b[k])
dp[i][r] = min(dp[i][r], dp[i + 1][k] + dp[k + 1][r]);
}
}
}
for(int i = 0; i < l; ++ i)
{
ans[i] = dp[0][i];
if(a[i] == b[i])
{
if(i == 0)
ans[i] = 0;
else
ans[i] = min(ans[i], ans[i - 1]);
}
else
{
for(int j = 0; j < i; ++ j)
ans[i] = min(ans[i], ans[j] + dp[j + 1][i]);
}
}
}
int main(void)
{
while(scanf("%s%s", a, b) != EOF)
{
int l = strlen(a);
solve(l);
printf("%d\n", ans[l - 1]);
}
return 0;
}
区间dp的求解思路
对于区间dp,可以这样考虑,对于区间[i, j],dp[i][j]可以怎样由更小的区间得到,枚举区间的分割点将区间分成更小的部分使得dp[i][j]可由更小区间上的dp值来递推得到,而区间的分割点在各个问题中通常都有着它对应的实际意义。
三、树形dp
参考博客地址 树形dp参考博客地址
1. hdu 1520 Anniversary party
题意:每个人都有有唯一一个直接管理者,每个人都有一个权重,从所有人中选取若干人,使得每个人和他的直接管理者不同时出现,选出一些人使权重总值最大。
思路:所有人构成一个树形结构,求在树上选出一些结点,使得结点和他的父节点不同时出现,求最大权值和。设dp[i][0]表示以i为根结点且不选i的子树最大权值和,dp[i][1]表示以i为根结点且选i的子树最大权值和。则,dp[i][1] = sum{dp[j][0]} + a[i],dp[i][0] = sum{max(dp[j][0], dp[j][1])},其中j为i的所有子结点,使用递归计算即可。
注意:树形dp在递推过程中,对于每个结点的状态可以再开一维来表示,比如这里第二维表示该结点是否选择。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
vector <int> g[6005];
int a[6005], dp[6005][2], indeg[6005];
int dfs(int x, int op)
{
if(dp[x][op] != -1)
return dp[x][op];
dp[x][1] = a[x];
dp[x][0] = 0;
for(int i = 0; i < g[x].size(); ++ i)
{
int v = g[x][i];
dp[x][1] += dfs(v, 0);
dp[x][0] += max(dfs(v, 0), dfs(v, 1));
}
return dp[x][op];
}
int main(void)
{
int n, u, v;
while(scanf("%d", &n) != EOF)
{
memset(dp, -1, sizeof(dp));
memset(indeg, 0, sizeof(indeg));
for(int i = 1; i <= n; ++ i)
scanf("%d", &a[i]);
for(int i = 1; i <= n; ++ i)
g[i].clear();
scanf("%d%d", &u, &v);
while(u + v != 0)
{
g[v].push_back(u);
indeg[u] ++;
scanf("%d%d", &u, &v);
}
int root = 0;
for(int i = 1; i <= n; ++ i)
{
if(indeg[i] == 0)
{
root = i;
break;
}
}
int ans = max(dfs(root, 0), dfs(root, 1));
printf("%d\n", ans);
}
return 0;
}
2. hdu 2196 Computers
题意:有n台电脑,1号电脑为根节点,其余电脑形成一个树结构,求每一个结点到图中其余结点的最远距离。
思路:该题是景点的树形dp问题,求图中任意一个点到图中其余点的最长距离。如果枚举每个点并遍历树,那么复杂度将达到O(n * n),该问题利用树的性质可以提出O(n)的算法。
设dp[u][0]代表以u为根节点的子树中以u为起点的最大距离。dp[u][1]代表以u为根节点的子树中以u为起点的次大距离(其中这条路径u的下一个顶点与dp[u][0]那条路径中u的下一个顶点不同,也就是说如果u只有一个子节点v,但v有多个子节点的时候,dp[u][1]的值为0,这样设置的原因主要是为了dp[u][2]的计算)。dp[u][2]代表经过u的父结点到达其他结点的最大距离。
第一遍dfs,由叶子结点向根节点更新dp[u][1]和dp[u][0]的值。对于边(u, v),如果dp[v][0] + cost[u][v] > dp[u][0],那么dp[u][1] = dp[u][0], dp[u][0] = dp[v][0] + cost[u][v],否则如果有dp[v][0] + cost[u][v] > dp[u][1],那么dp[u][1] = dp[v][0] + cost[u][v]。由于dp[u][0]和dp[u][1]的计算是由叶节点推根节点,因此递归的dfs放在dp处理之前。
然后从根节点向叶节点更新dp[u][2]的值。对于边(u,v),如果v在u的最长路径路径子树里面,也就是有dp[v][0] + cost[u][v] = dp[u][0],那么dp[v][2] = max(dp[u][2], dp[u][1]) + cost[u][v],如果v不在u的最长路径子树里面,那么dp[v][2] = max(dp[u][2], dp[u][0]) + cost[u][v]。由于是由根节点推叶节点,因此递归的dfs在dp处理之后。
注意:(1)这个题用到了两次递归,一次从叶向根推,一次从根向叶推,两种推法的dfs放的位置不同,这是一个很好的例子。 (2)如果使用vector存边,开始时一定要清空vector。如果使用数组存边,开始时下标pt一定要清0。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int to[10005], nxt[10005], head[10005], cost[10005], pt = 0, dp[10005][3];
void add(int a, int b, int c)
{
to[++ pt] = b;
cost[pt] = c;
nxt[pt] = head[a];
head[a] = pt;
}
void dfs1(int x)
{
if(dp[x][0] != -1 || dp[x][1] != -1)
return;
dp[x][0] = 0;
dp[x][1] = 0;
for(int i = head[x]; i != 0; i = nxt[i])
{
int t = to[i];
int val = cost[i];
dfs1(t);
int ans = val + dp[t][0];
if(ans > dp[x][0])
{
dp[x][1] = dp[x][0];
dp[x][0] = ans;
}
else if(ans > dp[x][1])
dp[x][1] = ans;
}
}
void dfs2(int x)
{
for(int i = head[x]; i != 0; i = nxt[i])
{
int t = to[i];
int val = cost[i];
if(dp[t][0] + val == dp[x][0])
dp[t][2] = max(dp[x][2], dp[x][1]) + val;
else
dp[t][2] = max(dp[x][2], dp[x][0]) + val;
dfs2(t);
}
}
int main(void)
{
int n, v, w;
while(scanf("%d", &n) != EOF)
{
memset(head, 0, sizeof(head));
memset(nxt, 0, sizeof(nxt));
pt = 0;
for(int i = 1; i <= n; ++ i)
{
dp[i][0] = dp[i][1] = -1;
dp[i][2] = 0;
}
for(int i = 2; i <= n; ++ i)
{
scanf("%d%d", &v, &w);
add(v, i, w);
}
dfs1(1);
dfs2(1);
for(int i = 1; i <= n; ++ i)
printf("%d\n", max(dp[i][0], dp[i][2]));
}
return 0;
}
3. poj 3107 Godfather
题意:n个点构成一个树形结构,但是不知道每条边的方向,因此不能确定根节点。对于每一个结点,如果将其从图中去掉,那么会有一个剩余的最大连通分量中结点数量,所有结点中这个值最小的那个结点认为是根节点。求所有可能的根节点。
思路:在图中建立一个双向边的数,从图中1号结点开始遍历,只走那些没有走过的结点。设dp[i][0]代表在遍历过程中以i结点的所有直接子结点为根的子树中含有的结点数最多的子树的结点数量,dp[i][1]代表在遍历过程中以i为根的整个子树的结点总数。那么对于i的所有儿子结点j,有dp[i][0] = max{dp[j][1]}。遍历一遍以后,对于每个结点,从它出发向下没有遍历到的所有结点一定是另一个与该结点相连的连通分量,可以直接用n - dp[i][1]得到这个连通分量的结点数量。
注意:由于建立的是双向边,因此数组要开两倍。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int to[100005], nxt[100005], head[50005], pt = 0;
int dp[50005][2], vis[50005], res[50005];
void add(int a, int b)
{
to[++ pt] = b;
nxt[pt] = head[a];
head[a] = pt;
}
void dfs(int x)
{
vis[x] = 1;
for(int i = head[x]; i != 0; i = nxt[i])
{
int t = to[i];
if(vis[t] == 0)
{
dfs(t);
dp[x][1] += dp[t][1];
dp[x][0] = max(dp[x][0], dp[t][1]);
}
}
dp[x][1] ++;
}
int main(void)
{
int n, u, v;
while(scanf("%d", &n) != EOF)
{
memset(vis, 0, sizeof(vis));
memset(nxt, 0, sizeof(nxt));
memset(head, 0, sizeof(head));
pt = 0;
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n - 1; ++ i)
{
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1);
int ans = 0x3f3f3f3f, nr = 0;
for(int i = 1; i <= n; ++ i)
ans = min(ans, max(dp[i][0], n - dp[i][1]));
for(int i = 1; i <= n; ++ i)
{
if(max(dp[i][0], n - dp[i][1]) == ans)
res[++ nr] = i;
}
for(int i = 1; i < nr; ++ i)
printf("%d ", res[i]);
printf("%d\n", res[nr]);
}
return 0;
}