leetcode 传递信息 题目总结(动态规划,dfs,bfs)
传递信息
深度优先搜索
思想:
图的深搜;
如果深搜是一个人,那么他的性格一定倔得像头牛!他从一点出发去旅游,只朝着一个方向走,除非路断了,他绝不改变方向!除非四个方向全都不通或遇到终点,他绝不后退一步!因此,他的姐姐广搜总是嘲笑他,说他是个一根筋、不撞南墙不回头的家伙。
深搜很讨厌他姐姐的嘲笑,但又不想跟自己的亲姐姐闹矛盾,于是他决定给姐姐讲述自己旅途中的经历,来改善姐姐对他的看法。他成功了,而且只讲了一次。从那以后他姐姐不仅再没有嘲笑过他,而且连看他的眼神都充满了赞赏。他以为是自己路上的各种英勇征服了姐姐,但他不知道,其实另有原因……
深搜是这样跟姐姐讲的:关于旅行呢,我并不把目的地的风光放在第一位,而是更注重于沿路的风景,所以我不会去追求最短路,而是把所有能通向终点的路都走一遍。可是我并不知道往哪走能到达目的地,于是我只能每到一个地方,就向当地的人请教各个方向的道路情况。
为了避免重复向别人问同一个方向,我就给自己规定:先问北,如果有路,那就往北走,到达下一个地方的时候就在执行此规定,如果往北不通,我就再问西,其次是南、东,要是这四个方向都不通或者抵达了终点,那我回到上一个地方,继续探索其他没去过的方向。我还要求自己要记住那些帮过他的人,但是那些给我帮倒忙的、让我白费力气的人,要忘记他们。有了这些规定之后,我就可以大胆的往前走了,既不用担心到不了不目的地,也不用担心重复走以前的路。哈哈哈……
原文链接:https://www.cnblogs.com/ShallByeBye/p/11769071.html
这文字写的是相当的生动形象!
深搜的优缺点:
优点
- 能找出所有解决方案
- 优先搜索一棵子树,然后是另一棵,所以和广搜对比,有着内存需要相对较少的优点
缺点
- 要多次遍历,搜索所有可能路径,标识做了之后还要取消(因为有可能会重复访问,所以需要对结点做一些标记)。
- 在深度很大的情况下效率不高
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> edges(n);
for(auto edge : relation) {
int src = edge[0];
int dst = edge[1];
edges[src].push_back(dst);
}
int ans = 0;
//深度优先搜索
function<void(int,int)> dfs = [&](int index, int steps) {
if(steps==k) {
if(index==n-1) {
ans++;
}
return; //剪枝,停止继续搜索,最大深度为k
}
for(auto dst : edges[index]) {
dfs(dst,steps+1); //先根搜索,因为先判断的是index的值,然后在导入该值对应的dst结点。
}
};
dfs(0,0);
return ans;
}
广度优先搜索
思想:
图的广搜;
广搜的优缺点:
优点
- 对于解决最短或最少问题特别有效,而且寻找深度小
- 每个结点只访问一遍,结点总是以最短路径被访问,所以第二次路径确定不会比第一次短
缺点
- 内存耗费量大(需要开大量的数组单元用来存储状态)
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> edges(n);
for (auto edge : relation)
{
int src = edge[0];
int dst = edge[1];
edges[src].push_back(dst);
}
int ans = 0;
int steps = 0;
function<void(void)> bfs = [&]()
{
queue<int> que;
que.push(0);
while (!que.empty() && steps < k)
{
steps++;
int sz = que.size();
while (sz--)
{
int q = que.front();
que.pop();
for (auto dst : edges[q])
{
que.push(dst);
}
}
}
if (steps == k)
{
while (!que.empty())
{
if (que.front() == n - 1)
ans++;
que.pop();
}
}
};
bfs();
return ans;
}
动态规划
优缺点:相比于穷举法 - (以最短路径为例)
优点:
- 减少重复计算,时间复杂度相对于其他算法,优势很明显
- 计算中得到很多有用的中间过程
- 不仅得到出发点到终点的最短路径,还得到了中间点到终点的最短路径
缺点:
- 消耗空间大,当所给出范围很大时,堆栈中很可能并不能满足所需要的空间大小,往往对其的解决办法是降低数组维度,或者去除一些不必要的状态数等。
常见问题:
背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题
-
动态规划主要用于求解以时间划分阶段的动态过程的优化问题
- 一些与时间无关的静态规划(如线性规划、非线性规划):只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
-
给定k阶段状态变量x(k)的值后,如果这一阶段的决策变量一经确定,第k+1阶段的状态变量x(k+1)也就完全确定,即x(k+1)的值随x(k)和第k阶段的决策u(k)的值变化而变化,那么可以把这一关系看成(x(k),u(k))与x(k+1)确定的对应关系,用x(k+1)=Tk(x(k),u(k))表示。这是从k阶段到k+1阶段的状态转移规律,称为状态转移方程 。
\[x(k+1)=T_k(x(k),u(k)) \]
最优性原理实际上是要求问题的最优策略的子策略也是最优
时间复杂度=状态总数*每个状态转移的状态数*每次状态转移的时间
这个题目里,我们可以用dp(i,dst)表示第i阶段到达dst处的走法数,因此有:
src表示所有k结点相连的前向结点值。
开始代码如下:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> edges(n);
for (auto &edge : relation) {
int src = edge[0], dst = edge[1];
edges[dst].push_back(src);
}
vector<vector<int>> dp(k+1, vector<int>(n,0));
dp[0][0] = 1; //起始状态
for(int i = 1; i <= k; i++) {
for(int j = 0; j < n; j++) {
int sum = 0;
for(auto & src:edges[j]) {
sum += dp[i-1][src];
}
dp[i][j] = sum;
}
}
return dp[k][n-1];
}
时间复杂度: \(O(k*\sum_j edges[j].size())=O(k*relation.size())\)
空间复杂度:O(k*n)
程序中存在很多不必要的计算,例如edges数组的初始化,从时间复杂度可以看出来只需要两层循环即可, 第二层只需要枚举所有边。
优化后的程序:
int numWays(int n, vector<vector<int>>& relation, int k) {
vector<vector<int>> dp(k+1,vector<int>(n,0));
dp[0][0] = 1;
for(int i = 1; i <= k; i++) {
for(auto & rel : relation) {
dp[i][rel[1]] += dp[i-1][rel[0]];
}
}
return dp[k][n-1];
}