每做一次决策就可以得到解的一部分,当所有决策做完之后,完整的解就“浮出水面”了。在回溯法中,每次决策对应于给一个结点产生新的子树,而解的生成过程对应一棵解答树,结点的层数就是“下一个待填充位置”cur。

9.3.1 多段图的最短路

多段图是一种特殊的DAG,其结点可以划分成若干个阶段,每个阶段只由上一个阶段所 决定。

例题9-4 单向TSP(Unidirectional TSP, UVa 116)

给一个m行n列(m≤10,n≤100)的整数矩阵,从第一列任何一个位置出发每次往右、右 上或右下走一格,最终到达最后一列。要求经过的整数之和最小。整个矩阵是环形的,即第 一行的上一行是最后一行,最后一行的下一行是第一行。输出路径上每列的行号。多解时输 出字典序最小的。图9-5中是两个矩阵和对应的最优路线(唯一的区别是最后一行)。

5 6
3 4 1 2 8 6
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 8 6 4
5 6
3 4 1 2 8 6
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 1 2 3
2 2
9 10 9 10
1 2 3 4 4 5
16
1 2 1 5 4 5
11
1 1
19

思路

从第一列走到最后一列, 每个点有三个决策, 求最小权值和。 环形的处理就用取模运算, 或者递推时直接判断即可。

状态表示: f[i][j] 走到第 i 行, 第 j 列的点, 到达最右端还需要加上的最小权值。 状态计算: f[i][j] = min(f[i + 1][j] + a[i][j], f[i][j]) f[i][j] = min(f[i + 1][j + 1] + a[i][j], f[i][j]) if j+1>m j=1 f[i][j] = min(f[i + 1][j - 1] + a[i][j], f[i][j]) if j-1<1 j=m 边界条件:f[n][j] = a[n][j]

代码

const int N = 110;
int a[N][N];
int f[N][N];
int nxt[N][N];
int n, m;
 
int main()
{
    while(cin >> n >> m)
    {
    	REPe(i, 1, n)
	    {
	        REPe(j, 1, m)
	        {
	            cin >> a[i][j];
	        }
	    }
		mem(nxt, 0);
		mem(f,0);
	    int res = 0x3f3f3f3f, first;
	    neREP(j, m, 1)
	    {
	        REPe(i, 1, n)
	        {
	            if (j == m)
	                f[i][j] = a[i][j];
	            else
	            {
	                int rows[3] = {i, i - 1, i + 1};
	                if (i == 1)
	                    rows[1] = n;
	                if (i == n)
	                    rows[2] = 1;
	                sort(rows, rows + 3);
	                f[i][j] = 0x3f3f3f3f;
	                REPe(k, 0, 2)
	                {
	                    int v = f[rows[k]][j + 1] + a[i][j];
	                    if (v < f[i][j])
	                    {
	                        f[i][j] = v;
	                        nxt[i][j] = rows[k];
	                    }
	                }
	            }
	            if (j == 1 && f[i][j] < res)
	            {
	                res = f[i][j];
	                first = i;
	            }
	        }
	    }
	    cout << first;
	    for (int i = nxt[first][1], j = 2; j <= m; i = nxt[i][j], j++)
	        cout << " " << i;
	
	    cout << "\n" << res << "\n";
	}
    
 
    return 0;
}

9.3.2 0-1背包问题

完全背包

物品无限的背包问题。有n种物品,每种均有无穷多个。第i种物品的体积为Vi,重量 为Wi。选一些物品装到一个容量为C的背包中,使得背包内物品在总体积不超过C的前提下 重量尽量大。1≤n≤100,1≤Vi ≤C≤10000,1≤Wi≤106。

很像9.2节中的硬币问题,只不过“面值之和恰好为S”改成了“体积 之和不超过C”,另外增加了一个新的属性——重量,相当于把原来的无权图改成了带权图 (weighted graph)。这样,问题就变为了求以C为起点(终点任意)的、边权之和最大的路 径。 与前面相比,DAG从“无权”变成了“带权”,但这并没有带来任何困难,此时只需将某处 代码从“+1”变成“+W[i]”即可

例题9-5 劲歌金曲(Jin Ge Jin Qu [h]ao, Rujia Liu’s Present 6, UVa 12563)

dp 二维dp 01背包 如果问一个麦霸:“你在KTV里必唱的曲目有哪些?”得到的答案通常都会包含一首“神曲”:古巨基的《劲歌金曲》。

为什么呢?一般来说,KTV不会在“时间到”的时候鲁莽地把正在唱的歌切掉,而是会等它放完。例如,在还有 秒时再唱一首 分钟的歌,则实际上多唱了 秒。但是融合了 首歌曲的《劲歌金曲》长达 秒(5),如果唱这首,相当于多唱了秒!

假定你正在唱KTV,还剩 秒时间。你决定接下来只唱你最爱的 首歌(不含《劲歌金曲》)中的一些,在时间结束之前再唱一个《劲歌金曲》,使得唱的总曲目尽量多(包含《劲歌金曲》),在此前提下尽量晚的离开KTV。

输入和每首歌的长度(保证不超过 分钟),输出唱的总曲目以及时间总长度。输入保证所有 首曲子的总长度严格大于

Sample Input

2
3 100
60 70 80
3 100
30 69 70

Sample Output

Case 1: 2 758
Case 2: 3 777

思路

求能唱的数量最多, 且最多时求时间最长的, 显然是二维DP和这题很像 宠物小精灵之收服

因为最后肯定是留至少1秒点金曲是最优, 故这里直接把输入的t减去1, 输出时判断一下, 如果是-1说明一个都点不了, 都输出0, 否则就在输出时把金曲加上。

其他就是正常的01背包了。 先求数量最多的, 然后逆序遍历求用时最多的。 滚动数组优化 状态表示: f[j] 恰好用时为j的最优选法 状态计算: f[j] = max(f[j], f[j - t] + 1)

注意全初始化为 -0x3f 负无穷, 因为这里求的是恰好为j。

题目给的t最大是1e9, 但实际上用不到这么多, 题目有说一首歌最多3分钟, 故总时间最大也就 180*n + 678

代码

const int N = 55, T = 1e4 + 10;
int a[T];
 
int n, m;
 
int main()
{
    int K;
    cin >> K;
    for (int kase = 1; kase <= K; kase++)
    {
        mem(a, -0x3f);
        a[0] = 0;
        cin >> n >> m;
        m -= 1;
        int res = 0;
        for (int i = 1; i <= n; i++)
        {
            int t;
            cin >> t;
            for (int j = m; j >= t; j--)
            {
                a[j] = max(a[j], a[j - t] + 1);
            }
        }
 
        for (int i = 0; i <= m; i++)
            res = max(res, a[i]);
        int time = 0;
        for (int i = m; i >= 0; i--)
            if (a[i] == res)
            {
                time = i;
                break;
            }
        if (m != -1)
        {
            printf("Case %d: %d %d\n", kase, res + 1, time + 678);
        }
        else
            printf("Case %d: %d %d\n", kase, 0, 0);
    }
}