【问题标题】:Dynamic programming dependence on greater states动态规划对更大状态的依赖
【发布时间】:2017-03-07 18:14:08
【问题描述】:

有N个城市。每个城市都有一个类型 1 到 10(含)的传送。给你一个大小为 N 的数组,用整数表示每个城市的传送类型。例如。 1 3 5 3 8 9。目标是根据以下规则找到从数组中的第一个条目到最后一个条目所需的最少小时数:

  1. 您可以从每个城市(数组中的条目)移动到其左侧或右侧邻居(如果有的话),该操作需要 1 小时。
  2. 您可以从每个城市传送到另一个与您要传送的城市具有相同传送类型的城市。在上面的示例数组中,您可以从索引 1 的城市传送到索引 3 的城市,因为它们都具有相同类型的传送:3。此操作需要花费给定的 R 小时数。

我已经实现了一个动态编程解决方案,当最快的方式只是向前推进时,它工作得非常好。但在某些情况下,返回几个城市然后传送到前方会更快,从而最大限度地减少花费的时间。

这是我的算法失败的示例:例如。 2 1 3 4 6 8 5 7 4 2 3 5 2 1 3 4 3 5 6 7R = 2 正确答案:索引0(时间=0)->索引9(时间=2)->索引8(时间=3)->索引7(时间=4)->索引19(时间=6)。 我的算法会找到仅向前移动的最快方式,显然,正确的最快方式还包括向后移动。

到目前为止,这是我的代码:

#include <iostream>
using namespace std;

int main()
{
    int dp[200] = {};
    short ti[200] = {};
    int numCities, R;
    cin >> numCities >> R;

    for (int i = 0; i < numCities; i++)
    {
        short temp;
        cin >> temp;
        ti[i] = temp;
    }

    for (int i = 1; i < numCities; i++)
    {
        dp[i] = dp[i - 1] + 1;

        for (int x = i - 1; x >= 0; x--)
        {
            if (ti[x] == ti[i])
            {
                if (R + dp[x] < dp[i])
                     dp[i] = R + dp[x];
            }
        }
    }

    cout << dp[numCities - 1];

    return 0;
}

如何使我的算法适用于这种情况,其中较低的状态依赖于较大的状态?

编辑:我使用以下方式动态编程:对于每个城市,我计算给定起始状态dp[0] = 0 的最快到达它们的方法。 那么递归关系就是:dp[i] = min(dp[i - 1] + 1, dp[every other point with same teleport type] + R)

【问题讨论】:

标签: algorithm dynamic-programming


【解决方案1】:

动态规划适用于问题具有最优子结构的情况。也就是说,您必须找到一种方法来细分问题,以便可以将细分的最佳解决方案用作构建块,以找到整个问题的最佳解决方案。

上面,我看到你说你想使用动态编程。我看到了代码。我没有看到对您正在考虑的子问题的清晰解释。也就是说:对解决方案的概念性理解是正确进行动态编程的关键,而这正是您不提供的。

我的直觉是动态编程在这种情况下不是一个好方法,因为:

  • 重新排列数组中的条目会破坏有关局部运动的信息
  • 可以从起点移动到数组中的任何条目 - 一种固有的非局部性。

您可以通过使用嵌套循环来解决这些问题。这为您提供了一个 O(n^2) 时间解决方案。

但是,将此问题视为加权图遍历的一个实例,您可以在 O(n log n + m) 时间内使用 Dijkstra's algorithm 解决它(O(n) 遍历足以建立每个节点的邻居)其中 m 是考虑的边数(这里可以将其限制为 Θ(m) 通过认识到每种传送器类型只能使用一次)。为什么不这样做?

您可以尝试使用 A* 来改善运行时间,但我不相信这会在一维上提供很大的改进。

完成此操作的代码可能如下所示:

#include <iostream>
#include <queue>
#include <unordered_set>
#include <unordered_map>

typedef std::unordered_map<int, std::vector<int> > tele_network_t;

int Dijkstra(const std::vector<int> &graph, const tele_network_t &tn, const int R){
  //This whole mess makes the smallest elements pop off the priority queue first
  std::priority_queue<
    std::pair<int, int>,
    std::vector< std::pair<int, int> >,
    std::greater< std::pair<int, int> >
  > pq; //<distance, index>

  //Keeping track of the teleporters used allows us to speed up the algorithm by
  //making use of the theorem that each teleporter type will be used only once.
  std::unordered_set<int> teleporters_used; 

  //Keep track of the path
  std::vector<int> parent(graph.size(),-1); //Parent==-1 indicates an unvisited node

  //At 0 distance, place the 0th node
  pq.emplace(0,0);
  parent[0] = 0; //The only node whose parent is itself should be node 0

  while(!pq.empty()){
    const auto c = pq.top();
    pq.pop();

    //We've reached the goal node
    if(c.second==graph.size()-1){
      std::cout<<"Dist = "<<c.first<<std::endl;
      break;
    }

    //Insert neighbours
    if(c.second!=0 && parent[c.second-1]==-1){ //Left neighbour
      parent[c.second-1] = c.second;
      pq.emplace(c.first+1,c.second-1);
    }
    if(parent[c.second+1]==-1){ //Right neighbour: can't overflow because goal is the rightmost node
      parent[c.second+1] = c.second;
      pq.emplace(c.first+1,c.second+1);
    }

    //Inner loop is executed once per teleporter type
    if(teleporters_used.count(graph[c.second])==0)
      for(const auto i: tn.at(graph[c.second])){
        if(parent[i]==-1){
          pq.emplace(c.first+R,i);
          parent[i] = c.second;
        }
      }

    teleporters_used.insert(graph[c.second]);
  }

  //Trace our steps backwards to recover the path. Path will be reversed, but a
  //stack could be used to fit this.
  int p = graph.size()-1;
  while(parent[p]!=p){
    std::cout<<p<<std::endl;
    p = parent[p];
  }
  std::cout<<0<<std::endl;
}

int main(){
  tele_network_t tele_network;

  const int R = 2;
  std::vector<int> graph = {{2,1,3,4,6,8,5,7,4,2,3,5,2,1,3,4,3,5,6,7}};

  //Determine network of teleporters
  for(int i=0;i<graph.size();i++)
    tele_network[graph[i]].push_back(i);

  Dijkstra(graph, tele_network, 2);
}

【讨论】:

  • DP 或许可以在这里工作,但我个人认为没有办法。根据我的回答,我认为您可能误解了数据。它被排列成一个数组,但如果你画出将所有东西连接在一起的箭头,你会发现它实际上更像是一朵云。 DP 在您可以利用数据的线性(或 2D)排列或您可以将数据视为树的情况下运行良好。较低/较大的州可能是指这棵树。
  • 阅读memoization 可能会有所帮助。本质上,DP 展开调用图记忆漏洞利用,以完全避免递归调用。
  • @PaulHankin:这是因为 m 仅测量所考虑的边数。 2 2 2 2 2 ... 的输入名义上具有 O(n^2) 边,但只需要考虑 O(n),因为第一次使用传送器有效地消除了O(n^2-n) 个边缘不予考虑。我在上面添加了代码并标记了允许您跳过的内部循环。
  • 但是您不需要知道搜索中每个节点使用了哪些传送器吗?我没有仔细查看您的代码,但是如果到目前为止到达集合中的任何节点的任何路径都使用了传送器,那么您似乎就被淘汰了传送器。
  • @PaulHankin:每个节点只能使用一个传送器,它所连接的传送器集合可以被视为一个集合。因此,虽然您确实需要知道在搜索中的每个节点使用了哪些传送器,但您不必单独考虑它们。
猜你喜欢
  • 2012-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-12
相关资源
最近更新 更多