Tour UVA - 1347 (旅行,dp)

John Doe, a skilled pilot, enjoys traveling. While on vacation, he rents a small plane and starts visiting
beautiful places. To save money, John must determine the shortest closed tour that connects his
destinations. Each destination is represented by a point in the plane pi =< xi
, yi >. John uses the
following strategy: he starts from the leftmost point, then he goes strictly left to right to the rightmost
point, and then he goes strictly right back to the starting point. It is known that the points have
distinct x-coordinates.
Write a program that, given a set of n points in the plane, computes the shortest closed tour that
connects the points according to John’s strategy.
Input
The program input is from a text file. Each data set in the file stands for a particular set of points. For
each set of points the data set contains the number of points, and the point coordinates in ascending
order of the x coordinate. White spaces can occur freely in input. The input data are correct.
Output
For each set of data, your program should print the result to the standard output from the beginning
of a line. The tour length, a floating-point number with two fractional digits, represents the result.
Note: An input/output sample is in the table below. Here there are two data sets. The first one
contains 3 points specified by their x and y coordinates. The second point, for example, has the x
coordinate 2, and the y coordinate 3. The result for each data set is the tour length, (6.47 for the first
data set in the given example).
Sample Input
3
1 1
2 3
3 1
4
1 1
2 3
3 1
4 2
Sample Output
6.47
7.89

打了很多废话,是给自己看的,可以跳过。

题意: 起点到终点再从另外一条路回来的最短长度。
思路: 将题目转换成两个人走两条不同的路到达终点。
个人认为紫书上的状态定义和实际实现不同。紫书状态定义是dp[i][j]代表经过小于max(i,j)的最小距离数。但是这个状态定义下,你的子状态不好找。

我们只知道一个状态可以变成什么状态,也就是dp[i][j]可以转移到dp[i+1][i]和dp[i+1][j],但是不好找是由什么转移过来的(有的时候也好找,直接把下标逆推一下,但是此处是dp[i+1][i]子状态对应dp[i][j],这个j怎么弄不好整)。当然,刷表法也可以解决这个问题

不过无论如何,通过这道题加深了对dp推导过程的理解
同时涉及了逆推、填表、刷表三种思路。可以想象成一个DAG图,每个点连接下一阶段的两个点,代表关系。其中 i i i 作为阶段, d p [ i ] [ j ] dp[i][j] dp[i][j]至于 d p [ i − 1 ] [ k ] dp[i-1][k] dp[i1][k]有关,这就是无后效性的体现。而既然是无后效性,那么子结构一定算出来了,而你的状态定义是最优的,那么已经算出来的子结构一定是最优的,那么满足最优子结构。可以看出来,最优子结构和无后效性是同一个东西的不同说法。

再放到图上看,是一张"分层"的DAG图,以 i i i为层次,对所有有 i i i的节点算完,才能继续算 i + 1 i+1 i+1的节点,而含有 i i i的节点也只依赖于含有 i − 1 i-1 i1的节点。

i i i就是阶段,也就是 f o r for for的一层循环。联想到floyd算法,必须先算完k,也就不难理解了。
除了阶段每个节点还有其他信息(算作 j j j, k k k之类)。
节点之间的关系可以用有向边来表示。

再说到逆推,我们已经知道正推的状态定义和转移关系,逆推的话就是把转移关系全部反过来,由此再反推状态定义,初始化最终状态,然后反推回起始状态。这个过程是需要改变状态定义的,但是尽量不要改变状态之间的关系,而只是将关系反向。

因为DAG图是一颗树,任意一个节点可以作为根,只要你的边没变,那么每个节点只要从后继状态转移过来即可。

再说说记忆化搜索中的逆推,那个逆推本身不改变递推的关系和方向,只不过是写法是递归的写法,使得栈帧一层层堆积,直到计算到一个已知的状态,再回溯计算栈帧。这样的转移方式特别适用于树结构,因为树结构的点不是表状的,简单的for枚举算不了。

状态定义: dp[i][j] 大于等于max(i,j)的路都走过了,还需要走多远。
子状态:dp[i+1][j]:第一个人走一格,dp[i+1][i]:第一个人走一格,第二个人走到i。
边界状态:i=n-1,此时dp状态可以直接计算出来。
状态转移:子状态加上距离取最小
为什么:代码中第一层循环是逆序的,因为我们能够直接获得的状态是i=n-1时,这与我们的状态定义有关。那么之后的状态都由此转移而来。想象一下一个dp数组所代表的矩阵,最后一行都直接计算出来了,倒数第二行就直接从倒数第一行转移而来,倒数第三行由倒数第二行转移而来,类似填表?通过矩阵画出状态转移的形式或许更好理解,01背包中也是如此,如果开二维数组的话正序逆序没关系,因为子状态在上一行,都计算出来了。但如果改成滚动数组就得注意循环的先后性了。我们需要的子状态是dp[i-1][j],dp[i-1][j-w[i]]。从右往左的话,遇到了dp[j],由于还没有计算,还是表示dp[i-1][j] 。而从后往前的话,dp[j-w[i]]是后计算,也是表示dp[i-1][j-w[i]]。所以可以这样转移。反之从左到右,遇到dp[j]的时候dp[j-w[i]]已经计算完了,变成了dp[i][j-w[i]],相当于已经选过物品了,从选过物品的状态转移过来,就成了完全背包。
学艺不精,口胡一下。

填表法和刷表法

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

typedef long long ll;

const int maxn = 1005;

double dist[maxn][maxn];
double dp[maxn][maxn];
int n;

struct Point {
    double x,y;
}a[maxn];

double get_dist(Point a,Point b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

void solve1() { //刷表法
    dp[1][1] = 0;
    for(int i = 1;i <= n;i++) {
        for(int j = 1;j <= i;j++) {
            if(i == j && i > 1 && i < n)continue;
            if(i < n) {
                dp[i+1][j] = min(dp[i+1][j],dp[i][j]+dist[i][i+1]);
                dp[i+1][i] = min(dp[i+1][i],dp[i][j]+dist[i+1][j]);
            }
            else {
                dp[n][n] = min(dp[n][n],dp[n][j] + dist[j][n]);
            }
        }
    }
}

void solve2() { //填表法
    dp[2][1] = dist[1][2];
    for(int i = 3;i <= n;i++) {
        for(int j = 1;j < i;j++) {
            if(i != j - 1) dp[i][j] = min(dp[i][j],dp[i - 1][j] + dist[i][i - 1]);
            dp[i][i - 1] = min(dp[i][i - 1],dp[i - 1][j] + dist[i][j]);
        }
    }
    dp[n][n] = dp[n][n - 1] + dist[n][n - 1];
}

int main() {
    while(~scanf("%d",&n)) {
        memset(dp,0,sizeof(dp));
        for(int i = 1;i <= n;i++) {
            scanf("%lf%lf",&a[i].x,&a[i].y);
        }
        for(int i = 1;i <= n;i++) {
            for(int j = 1;j <= n;j++) {
                dist[i][j] = get_dist(a[i],a[j]);
            }
        }
        
        for(int i = 1;i <= n;i++) {
            for(int j = 1;j <= n;j++) {
                dp[i][j] = 1e9;
            }
        }
        solve1();
//        solve2();
        
        printf("%.2f\n",dp[n][n]);
    }
    return 0;
}

#include
#include
#include
#include

using namespace std;

const int maxn = 1005;
double dp[maxn][maxn];
double dis[maxn][maxn];

struct Point
{
    double x,y;
}points[maxn];

double getdist(Point a,Point b)
{
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

int main()
{
    int n;
    while(~scanf("%d",&n))
    {
        for(int i = 1;i <= n;i ++)
        {
            scanf("%lf%lf",&points[i].x,&points[i].y);
        }
        
        for(int i = 1;i <= n;i++)
        {
            for(int j = 1;j <= n;j++)
            {
                dis[i][j] = getdist(points[i],points[j]);
            }
        }
 
        for(int i = n - 1;i >= 2;i--)//填表,i=n-1时候的状态可以直接计算出来,那么之后的状态都由次转移来。
        {
            for(int j = 1;j < i;j++)
            {
                if(i == n - 1)
                    dp[i][j] = dis[n-1][n] + dis[j][n];
                else dp[i][j] = min(dis[i][i+1] + dp[i+1][j], dis[j][i+1] + dp[i+1][i]);
            }
        }
        printf("%.2f\n",dp[2][1] + dis[1][2]);
    }
    return 0;
}

你可能感兴趣的:(#,线性dp)