2018SCUACM Training 6 动态规划

本文深入浅出地介绍了动态规划的基本概念及其在不同场景中的应用,包括最大上升子序列之和、三角形路径最大和等问题,并提供了详细的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

emmmm第一次这么早打完训练赛

但是还是想说

我好菜啊


动态规划,简称为DP问题(Dynamic Programming)

先说说我对动态规划的理解

我们处理问题的时候,可以把每一个问题对应到一种状态上,这个问题的解也就是这个状态对应的状态函数的值。

在动态规划问题中,我们就是通过研究问题在不同状态中的转移,来将问题不断的简化,最终到达一种显而易见可以求解的状态。

解决动态规划问题的核心,首先是确定状态,然后是找到状态转移方程,通过应用状态转移方程来实现将复杂的状态转移到简单的状态,通过直接得到简单状态的状态函数值,进而得到复杂状态的状态函数值,从而解决复杂问题。

好吧抽象的东西看不懂没关系emmmm反正我的理解可能也不完全,还是直接看题目吧。


A - The Largest Increasing Subsequences

zh成功的在他人的帮助下获得了与小姐姐约会的机会,同时也不用担心被非“川大”的女票发现了,可是如何选择和哪些小姐姐约会呢?zh希望自己可以循序渐进,同时希望挑战自己的极限,我们假定每个小姐姐有一个“攻略难度值”

从攻略成功第一个小姐姐开始,zh希望每下一个需要攻略的小姐姐难度更高,同时又希望攻略难度值之和最大,好了,现在小姐姐们排成一排,zh只能从左往右开始攻略,请你帮助他找到最大的攻略难度和

input

多组输入,每组数据占一行,每行一个整数n表示小姐姐个数,接着n个数a1,a2,...,ana1,a2,...,an表示第i个的小姐姐攻略难度 (a_i在32位有符号整型范围内),n = 0表示输入结束 (0 <= n <= 1000)。

output

一个数,最大攻略和

Sample Input

3 1 3 2

4 1 2 3 4

4 3 3 2 1

0

Sample Output

4

10

3


看到这题,我第一个想到的是和它相似的一个题:求最长上升子序列:

给定一个长度为n的序列,求出其最长上升子序列的长度。

首先我们明确最长上升子序列是什么。最长就不用解释了,上升子序列即是对子序列中的任何两个元素ai,ajai,aj,当i<ji<j时,总有ai<ajai<aj,最后,子序列是区别于子段而言的概念,子序列和子段都是在原序列中【按顺序】选取一定的元素组成的新序列;但是子序列中的元素选取是任意的,而子段必须保证连续。如原序列为abcde,则bcd是其子段,也是子序列,然而acd不是其子段(不连续),是子序列。

A题要求的是最大上升子序列之和,而我刚刚提到的那题是求最长上升子序列长度。其实是非常相似的两道题。在这里我只解释A题,而最长上升子序列就自己思考吧。

首先我们从动态规划的角度来考虑这个问题。怎么考虑呢?我们可以认为:

以第n个元素结束的最大上升子序列为状态为dp[n]

那么状态转移方程当然就是与n有关的状态了,这些状态可能是n/2,可能是n-1,也有可能是-n,max-n等等,或者与其中的多个都有关。具体要怎么考虑状态之间的转移,还是要依据题目来决定。

在这里我们这么来考虑这个问题,以第n个元素结束的上升子序列,要么只有这一个元素,要么就是从以前的一个上升子序列延伸而来。而在其中的【最大】上升子序列,就是在上面所有状态中的最大值。

所以,我们的状态转移方程为:

dp[i] = max(dp[i], dp[j]+a[i]), j<i&amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;a[j]<a[i]j<i&amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp;a[j]<a[i]

有了这个方程,我们就相当于有了一个递归表达式,自然可以用递归来解决这个问题。但是递归往往对时间空间要求很高,在绝大部分的dp问题中,我们都不会采用递归,而是采取递推的方式来解决。

为了求出dp[i],我们必须首先预支所有在第i个位置之前的dp值,因为你不知道你可能会用哪个dp[j]。所以在求解这个问题时,我们从小到大更新dp数组,最后dp数组中的最大值,即为我们问题的答案。

emmmmm其实对于接触过一点dp的人来说这个就是个非常容易的板子题。。上面讲那么多只是试试能不能把dp解释清楚(可能也并没有解释的特别好),所以后面的题目不会再那么细致。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1005
using namespace std;
int a[maxn];
int dp[maxn];
int main() {
//  freopen("in.txt","r",stdin);
    int n;
    while(cin>>n) {
        if(n == 0) break;
        memset(dp,0,sizeof(dp));
        for(int i = 0; i < n; i++) cin>>a[i];
        for(int i = 0; i < n; i++) {
            dp[i] = a[i];
            for(int j = 0; j < i; j++)
                if(a[j] < a[i]) dp[i] = max(dp[i], dp[j]+a[i]);
        }
        int MAX = dp[0];
        for(int i = 1; i < n; i++)
            MAX = dp[i] > MAX ? dp[i] : MAX;
        cout<<MAX<<endl;
    }
    return 0;
}

B - Triangles

The cows don’t use actual bowling balls when they go bowling. They each take a number (in the range 0..99), though, and line up in a standard bowling-pin-like triangle like thi:

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

Then the other cows traverse the triangle starting from its tip and moving “down” to one of the two diagonally adjacent cows until the “bottom” row is reached. The cow’s score is the sum of the numbers of the cows visited along the way. The cow with the highest score wins that frame.

Given a triangle with N (1 <= N <= 350) rows, determine the highest possible sum achievable.

Input

Line 1: A single integer, N

Lines 2..N+1: Line i+1 contains i space-separated integers that represent row i of the triangle.

Output

Line 1: The largest sum achievable using the traversal rules

Sample Input

5

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

Sample Output

30

Hint

Explanation of the sampl:

        7
       *
      3   8
     *
    8   1   0
   *
  2   7   4   4
   *
4   5   2   6   5

The highest score is achievable by traversing the cows as shown above.


题目大意:从最上面的结点开始走,每次走可以在下面一层的左右两个结点之一选择,求从最上层到最下层的路径值的最大和。

我们记dp[i][j]表示到达第i行的第j个元素时的路径最大和,则我们要求的结果就是dp的最后一行中的最大值。

显然,第i行第j个元素只能从第i-1行第j个或第j-1个元素过来,所以dp[i][j]的值就是他们之间的最大值再加上a[i][j]的值

即状态转移方程为:

dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j];

其实照这个公式开个二维数组已经足以解决这个问题了。但是我们还有办法可以在空间上对其进行优化。

这里利用了类似滚动数组的思想,因为我们要求第i行的元素时,只需要知道第i-1行的元素即可,再往前的信息并不重要,所以实际上我们只需要保存两行的信息就够了。

更进一步,其实我们都没必要保存两行信息,因为第i行的第j个元素只会影响到第i+1行的第j+1个元素和第j个元素,一旦这两个元素更新完那么dp[i][j]也就没用了。在此基础上,我们把这两行压缩为1行,在更新dp[j+1]的时候并不会影响dp[j]的信息,而更新dp[j]的之后就相当于把没用的第j个元素用新值给覆盖掉了。

所以我们新的状态转移方程为:

dp[j] = max(dp[j], dp[j-1]) + a[i][j];

使用这个状态转移方程的前提是要求从上往下,从后往前更新dp。

这个和上周的背包问题的写法思路是一样的,如果不能理解的话用二维的dp并不会对时间有太大影响,只是会浪费一些不必要的空间而已。代码肯定是能通过的。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 355
using namespace std;
int a[maxn];
int dp[maxn];
int main() {
//  freopen("in.txt","r",stdin);
    int n;
    while(cin>>n) {
        memset(dp,0,sizeof(dp));
        for(int i = 0; i < n; i++) {
            for(int j = 0; j <= i; j++) cin>>a[j];
            for(int j = i; j > 0; j--)
                dp[j] = max(dp[j], dp[j-1]) + a[j];
            dp[0] = dp[0] + a[0];
        }
        int MAX = dp[0];
        for(int i = 1; i < n; i++)
            MAX = dp[i] > MAX ? dp[i] : MAX;
        cout<<MAX<<endl;
    }
    return 0;
}

C - Apples, Pens and Pineapples

Description

有两棵APP树,编号为1,2.每一分钟,这两棵APP树中的其中一棵会掉一个APP.每一秒,你可以选择在当前APP树下接APP,或者迅速移动到另外一棵APP树下接APP(移动时间可以忽略不计),但由于却乏锻炼,你最多移动W次.问在T秒内,你最多能收集多少个APP.假设你开始站在1号APP树下.

Input

第1行:两个整数T(1 < = T< = 1000)和W(1 < = W< = 30)

第2..T+1行:1或2,代表每分钟掉落APP的那棵树的编号

Output

一行一个整数,代表你移动不超过W次能接住的最大APP数

Sample Input

7 2

2

1

1

2

2

1

1

Sample Output

6


记dp[i][j]为当前在位置j,剩余i次移动,可以得到的最多苹果数目

当在位置j掉落一个苹果时,我们就更新一次dp数组。

在位置dp[i][j]的状态只能从两种状态转移而来:第一种:掉落苹果时不移动,仍然在dp[i][j]的状态;第二种:从dp[i+1][!j]的状态开始,掉落苹果时从!j的位置移动到j的位置,消耗1次移动机会,到达dp[i][j]。

即状态转移方程为:

dp[i][j] = max(dp[i][j], dp[i+1][!j]) + 1

因为我们的dp是掉落一次苹果更新一次,所以我们最终的答案不是dp[i][j],而是dp数组中的最大值。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxt 1005
#define maxw 35
using namespace std;
int dp[maxw][2];
int main() {
//  freopen("in.txt","r",stdin);
    int t,w;
    while(cin>>t>>w) {
        memset(dp,0,sizeof(dp));
        for(int i = 0; i < t; i++) {
            int k; cin>>k; k--;
            for(int j = 0; j <= w; j++)
                dp[j][k] = max(dp[j][k], dp[j+1][1-k]) + 1;
        }
        int MAX = -1;
        for(int i = 0; i < 2; i++)
        for(int j = 0; j <= w; j++)
            MAX = dp[i][j] > MAX ? dp[i][j] : MAX;
        cout<<MAX<<endl;
    }
    return 0;
}

There are times you recall a good old friend and everything you’ve come through together. Luckily there are social networks — they store all your message history making it easy to know what you argued over 10 years ago.

More formal, your message history is a sequence of messages ordered by time sent numbered from 1 to n where n is the total number of messages in the chat.

Each message might contain a link to an earlier message which it is a reply to. When opening a message x or getting a link to it, the dialogue is shown in such a way that k previous messages, message x and k next messages are visible (with respect to message x). In case there are less than k messages somewhere, they are yet all shown.

Digging deep into your message history, you always read all visible messages and then go by the link in the current message x (if there is one) and continue reading in the same manner.

Determine the number of messages you’ll read if your start from message number t for all t from 1 to n. Calculate these numbers independently. If you start with message x, the initial configuration is x itself, k previous and k next messages. Messages read multiple times are considered as one.

Input

The first line contains two integers n and k (1 ≤ n ≤ 105, 0 ≤ k ≤ n) — the total amount of messages and the number of previous and next messages visible.

The second line features a sequence of integers a1, a2, …, an (0 ≤ ai < i), where ai denotes the i-th message link destination or zero, if there’s no link from i. All messages are listed in chronological order. It’s guaranteed that the link from message x goes to message with number strictly less than x.

Output

Print n integers with i-th denoting the number of distinct messages you can read starting from message i and traversing the links while possible.

Examples


Input

6 0

0 1 1 2 3 2

Output

1 2 2 3 3 3


Input

10 1

0 1 0 3 4 5 2 3 7 0

Output

2 3 3 4 5 6 6 6 8 2


Input

2 2

0 1

Output

2 2


Note

Consider i = 6 in sample case one. You will read message 6, then 2, then 1 and then there will be no link to go.

In the second sample case i = 6 gives you messages 5, 6, 7 since k = 1, then 4, 5, 6, then 2, 3, 4 and then the link sequence breaks. The number of distinct messages here is equal to 6.


emmmm题意有点难理解,而且我觉得note给的样例解释是错的。。应该是访问5,6,7之后继续访问4,5,6,然后3,4,5,最后2,3,4,总共6个元素

题目大意:你现在有n条消息,每条消息会链接到之前的某条消息,如果没有链接则链接到0。当你访问一条消息时,你可以看到这条消息左右各k条,包括当前的消息一共2k+1条消息。

当访问这条消息时,如果这条消息链接到了之前的某条消息,那么你就相当于在访问到这个消息后再访问它所链接到的消息。

求当你访问第i条消息时,你能看到的所有消息数目。同一条消息被重复看到只计算一次。要输出i从1到n的所有值。

比如note解释的样例2,当i=6时,由于k=1,故你能看到5,6,7三条消息,然后第6条消息链接到第5条消息,所以你继续访问第5条消息,从而你能看到4,5,6三条消息,进一步,第5条消息链接到第4条消息,所以你能看到3,4,5,然后4->3,你能看到2,3,4,最后3->0,即没有链接的消息了,访问结束。此时你能看到的所有消息为2,3,4,5,6,7一共6条。

这个题目着实有点难,不知道是不是我思路有问题,反正我wa了7发才A到。

暴力的方法就不说了,直接从非暴力说起。

刚开始我考虑的是,在前i-1条消息之后添加第i条消息,会对b[i]有什么影响(b[i]表示访问第i条消息能看到的消息数目)。

如果之前我们只考虑了前i-1条消息,那么在添加第i条消息后,[i-k,i)这个区间内的消息能看到的消息数目就会+1。然后第i条消息能访问到的消息数为:a[i]访问到的消息数+i访问到的消息数-重合的部分,也就是:

b[i] = b[a[i]] + k+1 - (a[i]+k-(i-k)+1)

上面的关系式严格说来并不严谨,因为没考虑到越过[1,n]边界的情况,但是能比较直观的说明上面的逻辑。如果考虑边界的话状态转移方程应为:

b[i] = b[a[i]] + max(i - (a[i]==0 ? max(1,i-k) : max(a[i]+k+1,i-k)) + 1, 0)

这个式子看起来逻辑比较严谨,但看起来就不那么直观了。

或者写成循环的形式:

b[1] = 1;
for(int i = 2; i <= n; i++) {
    for(int j = max(1, i-k); j < i; j++) b[j]++;
    b[i] = b[a[i]];
    for(int j = a[i] == 0 ? max(1, i-k) : max(a[i]+k+1, i-k); j <= i; j++) b[i]++;
}

但是遗憾的是,这个写法交上去居然T了,而且是评测鸡跑的特别慢测到102组数据了满以为要AC的时候给我了个TLE。

其实这个方法我觉得已经很简单了,既然还超时了,那就意味着我的很多b[j]++的过程是可以用数学方法替代的。

之后就想到了另一种考虑方法:访问一条消息能看到的消息分为两部分,第一部分是链接到之前的消息能看到的部分,另一部分是当前消息左右k条能看到的消息,这两部分还有可能重合。

记c[i]为第i条消息能看到的左右k条部分的消息,则有:c[i] = min(n, i+k)-max(1, i-k)+1;

此时b[i]的递推式为:

b[i] = b[a[i]] + c[i] - 重合的部分;

重合的部分 = max(0, min(n, a+k)-max(1, i-k)+1));

当a[i]=0时b[i]还要特殊考虑,此时b[i]=c[i];

这样就根本不需要什么循环了,直接通过纯数学表达式就能计算出结果。

所以最终代码为:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 100005
int a, b[maxn];
using namespace std;
int main() {
    int n,k;
    b[0] = 0;
    while(cin>>n>>k) {
        for(int i = 1; i <= n; i++) {
            cin>>a;
            b[i] = min(n, i+k) - max(1, i-k) + 1 + (a == 0 ? 0 : (b[a] - max(0, min(n, a+k)-max(1, i-k)+1)));
            cout<<b[i]<<" ";
        }   cout<<endl;
    }
    return 0;
}

E - The Longest Non-decreasing Subsequences

Description

有一个由1和2构成的序列,你可以选择一个区间[l,r] (1≤ l ≤ r ≤ n)翻转,即这段区间就变成ar,ar-1,…,al+1,al。使得新序列中的non-decreasing subsequence最长。

一个长为k的non-decreasing subsequence是,一个满足下标p1 < p2 < … < pk并且,ap1 ≤ ap2 ≤ … ≤ apk的子序列。

Input

第一行一个整数n (1 ≤ n ≤ 2000),表示原串的长度。

第二行包含n个整数,表示a1, a2, …, an (1 ≤ ai ≤ 2, i = 1, 2, …, n)。

Output

Print a single integer, which means the maximum possible length of the longest non-decreasing subsequence of the new sequence.

Examples


Input

4

1 2 1 2

Output

4


Input

10

1 1 2 2 2 1 1 2 2 1

Output

9


这题要考虑的是翻转一次是怎么对最长非递减子序列长度造成影响的。

容易贪心的想到,最优方案反转的区间一定是以2开始,以1结束。如果最优方案不满足这个条件的话反不反转对最长非递减子序列的长度没有影响。

因为如果这个序列以1开始,那么我不反转这个1一定不会让情况更糟糕,同样的,如果这个序列以2结尾,那么我不反转这个2也不会让情况更糟。

所以理想状况下,我们最终结果的最长子序列在原序列中是这样的一个形式:

1..1(2..1)2..2

其中括号括起来的是反转的部分

我们发现,这样的一个序列在原序列中其实就是两个非递减子序列!即:

(1..12..)(..12..2)

所以实际上我们只要考虑所有把原序列分成两段的情况,分别计算两段中的最长非递减子序列,并把他们相加即可。

记b[i]表示[0,i]内的最长非递减子序列长度,c[i]表示[i,n)内的最长非递减子序列长度,则我们求的就是b[i]+c[i+1]的最大值

至于如何求最长非递减子序列,就不是这题讨论的范围内了。可以去A题找找灵感,也可以比对一下我在A题中给出来的思考题,虽然我没有讲方法,但和A题是十分类似的。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 2005
//b[i]:以i为结束的最长非下降子序列的长度
//c[i]:以i为开始的最长非下降子序列的长度 
//反转操作作用在b序列的第一个2到c序列的最后一个1之间
//使得两个序列由1..2+1..2变为1..1.2..2 
//反转后最长的长度为b[i]+c[i+1] 
int a[maxn], b[maxn], c[maxn];
using namespace std;
int main() {
//  freopen("in.txt","r",stdin);
    int n;
    while(cin>>n) {
        for(int i = 0; i < n; i++) cin>>a[i];
        for(int i = 0; i < n; i++) {
            b[i] = 1;
            for(int j = 0; j < i; j++)
                b[i] = a[j] > a[i] ? b[i] : max(b[i], b[j]+1);
            c[n-i-1] = 1;
            for(int j = n-1; j > n-i-1; j--)
                c[n-i-1] = a[j] < a[n-i-1] ? c[n-i-1] : max(c[n-i-1], c[j]+1);
        }
        int MAX = b[0]+c[1];
        for(int i = 1; i < n-1; i++)
            MAX = max(MAX, b[i]+c[i+1]);
        cout<<MAX<<endl;
    }
    return 0;
}

F - Code Blocks

Description

你有一排砖,你想给这一排砖中的每一块涂上红,蓝,绿,黄中4种颜色中的其中1种,也就是说每一块砖只能涂1种颜色.现在一排有N块砖,问你有多少种方案,让红色和绿色的砖的块数为偶数.

Input

输入:第一行为T,代表数据组数.(1<=T<=100),接下来T行每行包括一个数字N,代表有N(1<=N<=10^9)块砖.

Output

输出:满足条件的方案数,答案模上10007.

Sample Input

2

1

2

2

6


这题emmmm我选择放弃。。。以后再来更。。。

因为这题我是暴力O(4n)O(4n)打表找出来公式再过的。。。至于怎么找的我想不起来了(草稿纸扔了)也不想去想了,因为方法实在是不怎么聪明,如果暴力打表的话说不定别人有更好的办法。。。

反正最后求出来通项公式是:

ans[i]=4i1+2i1ans[i]=4i−1+2i−1

并没有想通为什么

然后要模10007,其实就是预处理出2n2n对10007的模,发现有周期(肯定有周期的,打表找就好了),周期是10006

//方法数通项公式:ans[i] = 2^(2i-2)+2^(i-1) 
//a[i]记录2^i对10007的模,周期打表得之,为T=10006 
//预处理出2^i对mod的模
//ans[i]=(a[(2i-2)%T]+a[(i-1)%T])%mod 

//但是打表规律,不知道为什么通项公式如此 
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define mod 10007
#define TT 10006
using namespace std;
int a[TT];
void init() {
    a[0] = 1;
    for(int i = 1; i < TT; i++)
        a[i] = 2*a[i-1]%mod;
}
int main() {
    init();
    int T; cin>>T;
    int cnt = 1;
    while(T--) {
        int n; cin>>n;
        cout<<(a[(2*n-2)%TT]+a[(n-1)%TT])%mod<<endl;
    }
    return 0;
}

G - Bounty Hunters

Finally you found the city of Gold. As you are fond of gold, you start collecting them. But there are so much gold that you are getting tired collecting them.

So, you want to find the minimum effort to collect all the gold.

You can describe the city as a 2D grid, where your initial position is marked by an ‘x’. An empty place will be denoted by a ‘.’. And the cells which contain gold will be denoted by ‘g’. In each move you can go to all 8 adjacent places inside the city.

Input

Input starts with an integer T (≤ 100), denoting the number of test cases.

Each case will start with a blank line and two integers, m and n (0 < m, n < 20) denoting the row and columns of the city respectively. Each of the next m lines will contain n characters describing the city. There will be exactly one ‘x’ in the city and at most 15 gold positions.

Output

For each case of input you have to print the case number and the minimum steps you have to take to collect all the gold and go back to ‘x’.

Sample Input

2

5 5

x….

g….

g….

…..

g….

5 5

x….

g….

g….

…..

…..

Sample Output

Case 1: 8

Case 2: 4


题目大意:给你一个m*n的地图,要求找到一条路线,从x出发,到x结束,且经过所有的g。每次移动时可以走相邻的8个方向。求最短路线的长度。

这题我wa了12发才A掉,而且还参考了很多资料,个人认为在我的能力范围之外。

熟悉的人肯定就一眼看出来了,这是个旅行商问题,跟旅行商问题一毛一样,只是预处理方面要根据题目自己写。(旅行商问题是什么请自行百度)

之前我没写过旅行商问题,准确的说是跟图论有关,有半毛钱关系的题目我都没做过。写了个辣鸡剪枝的DFS超时之后就不知道该怎么办了。书上有个什么分支定界法(看了之后觉得本质还是DFS,以为是剪枝比较强),本来去年考试复习的时候都没看懂,结果现在要写。花了若干小时(绝对不止两个小时)对着书一步一步写完觉得很完美,结果比我原来的DFS感觉还跑得慢,而且交上去还wa了,至今不知道哪里写错了。

后来看了紫书才知道,原来这个题真的是DP,真的是DP,真的是DP。

dp(i,S)表示当前在点i处,还需要访问S中的所有点并回到起点,所需要的距离

dp[i][S] = min{dp[j][S-j] + dis[j]};

这个DP策略我是想不到的,仅限于看懂,所以也没能力深入分析,个人觉得这两句话还是比较清楚的,希望读者能看懂。

dp策略看懂了其实这题就做完一半了,为什么只是一般呢?因为你的S是集合啊,集合在程序里面是不可能做下标的对吧,所以我们要把集合数字化,具体怎么化的,我能说又超出我的能力吗,我在代码注释里写了一点,尽力了。。。

好吧这题吐槽比较多干货比较少,还是我太菜。我觉得我的注释很详细了(在我的能力范围内),所以就不啰嗦了。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxg 16
#define inf 0x3f3f3f3f
int dis[maxg][maxg];
int gx[maxg], gy[maxg];//黄金坐标 
int gcnt;//黄金数量 
int dp[maxg][1<<maxg];
using namespace std;
int main() {
//  freopen("in.txt","r",stdin);
    int T; cin>>T;
    for(int Case = 1; Case <= T; Case++) {
        int n,m; cin>>m>>n;//读入地图规模 
        gcnt = 0;
        for(int i = 0; i < m; i++)
        for(int j = 0; j < n; j++) {//读入地图数据 
            char c; cin>>c;//记录每个黄金的坐标(把起点也看作黄金)
            if(c == 'g' || c == 'x') {
                gx[gcnt] = i; gy[gcnt] = j; gcnt++;
            }
        }
        for(int i = 0; i < gcnt; i++)
        for(int j = 0; j <= i; j++)//处理两点之间距离 
            dis[i][j] = dis[j][i] = max(abs(gx[i]-gx[j]), abs(gy[i]-gy[j]));

//      dp(i,S)表示当前在点i处,还需要访问S中的所有点并回到起点,所需要的距离
//      dp[i][S] = min{dp[j][S-j] + dis[j]}; 
        memset(dp,0x3f3f3f3f,sizeof(dp));
        for(int i = 0; i < gcnt; i++) dp[i][0] = dis[i][0];//初始化空集对应的dp数组 
        for(int k = 1; k <= gcnt; k++) {//枚举所有含有k个点的集合,参考《挑战》P157 
            int s = (1<<k) - 1;//最小的k个元素的集合 
            while(s < 1<<gcnt) {//对每个k元子集进行处理 
                for(int i = 0; i < gcnt; i++) {//枚举起点 
                    if(!((s>>i) & 1))//如果i不在集合s中,才更新dp数组 
                        for(int j = 0; j < gcnt; j++)//枚举s中的所有元素 
                            if((s>>j) & 1)//如果第j个点在s中 
                                dp[i][s] = min(dp[i][s], dp[j][s - (1<<j)] + dis[i][j]);

                }
                //枚举n元集合的k元子集的方法:从最小的k元子集开始,把最后的一段连续的1清零并在左边添1,记长度为l,再将最后l-1位变1以保证1的数目不变 
                int x = s & -s, y = s+x;//x是s的最低位的1,y将s的最后一段连续的1清零并在左边添1,例如s=0101110,则x=0000010,y=0110000 
                s = ((s & ~y) / x>>1) | y;//s&~y,只保留在y中s改变的部分,即s中最后一段连续的1,即s&~y=0101110&1001111=0001110 
                                          //(s&-y)/x表示将最后一段连续的1移动到右端点,再向右移>>1表示删掉最后的1,维持1的数量不变
                                          //得到的结果与y并起来,符合题意(y中有k-l+1个1,上一步的结果中有l-1个1) 
            }
        }
        cout<<"Case "<<Case<<": "<<dp[0][(1<<gcnt)-2]<<endl;
    }
    return 0;
}

H - Sets

Description

找一些2^x(0<=x),使它们的和为N。比如,N=7:

1) 1+1+1+1+1+1+1

2) 1+1+1+1+1+2

3) 1+1+1+2+2

4) 1+1+1+4

5) 1+2+2+2

6) 1+2+4

(1 <= N <= 1,000,000).

Input

N.

Output

排列方式总数。由于这个数可能很大,只需要保留最后9位

Sample Input

7

Sample Output

6

Hint

打表的会被系统自动识别判为WA


终于又到了久违的最后一题。

看到这个问题我又想到了另一个问题:整数划分问题:

求将一个正整数n划分成m个整数的方法数(简称n的m划分数)。

比如3划分为2份可以是0+3,1+2共2种(相同数字的不同排列看成是同一种)

这两个题目只是题面有相似性,处理方法也有相似性。

还是提一下整数划分问题,整数划分问题考虑的是划分的有没有元素0,记dp[n][m]为n的m划分数,若划分里面含0,则转移到撞他dp[n][m-1],否则,每个数都至少为1,那么将m个数全部减1,转移到状态dp[n-m][m],所以其状态转移方程为:

dp[n][m] = dp[n][m-1] + dp[n-m][m];

具体细节就不再提了。

回到我们的H题,我首先看到H题的时候第一个想到的办法是背包,因为n的最大值才1e6,也就是2^20,相当于20个物品的完全背包,第i个物品的价值为a[i]=2ia[i]=2i

设dp[i][j]表示只使用前i个物品凑出j的方法数,那么

dp[i][j]=Σdp[i1][jka[i]],k=a[i]jdp[i][j]=Σdp[i−1][j−k∗a[i]],k=a[i]≤j

这个式子可以压缩到1维dp

所以用背包来解决这个问题的代码为:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1000005
#define mod 1000000000
using namespace std;
int a[20], dp[maxn];
void init() {
    a[0] = 1;
    for(int i = 1; i < 20; i++)
        a[i] = 2*a[i-1];
}
void solve(int m) {
    memset(dp,0,sizeof(dp));
    dp[0] = 1;
    for(int i = 0; i < 20; i++) {
        for(int j = a[i]; j <= m; j++)
            dp[j] = (dp[j] + dp[j-a[i]]) % mod;
    }
}
int main() {
    init(); solve(maxn);
    int n;
    while(cin>>n) cout<<dp[n]<<endl;
    return 0;
}

不过,这题还有另一种dp方案,这种dp方案思路上就与整数划分问题很相似了。

首先很容易想到的是,奇数的划分最终结果一定有一个1,所以如果n是奇数,那么dp[n]=dp[n-1]

如果n是偶数呢?那么有两种情况:第一种,分解的数中含有1,那么此时的状态转移到dp[n-1];第二种,分解的数中不含有1,那么说明所有的数都是2的倍数,这时候让所有的数都除以2,就转换到了dp[n/2]的状态。因此状态转移方程为:

dp[i] = i&1 ? dp[i-1] : dp[i-1]+dp[i/2];

这样对应的代码为:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 1000005
#define mod 1000000000
using namespace std;
int dp[maxn];
int main() {
    dp[0] = dp[1] = 1; dp[2] = 2;
    for(int i = 3; i < maxn; i++)
        //奇数一定与其前一个偶数组合方法数相同 
        if(i&1) dp[i] = dp[i-1];
        //偶数有两种分解方式:拿1,不拿1,
        //不拿1相当于以后最小只能拿2,
        //也就相当于i/2只能拿1的方案数 
        else dp[i] = (dp[i-1]+dp[i/2])%mod;
    int n; while(cin>>n) cout<<dp[n]<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值