【USACO题库】 动态规划 汇总(普及-/普及)

数据结构提高是够用了=-=虽然树状数组没学 但是其他类问题只能打到普及

普及啊啊啊!!!

而且这边省选组都是什么仙人掌啊,什么系什么点对啊...感觉数据结构并没有什么用 (实际上很有用但我不会用就是了)

然后颓到提高组来了..结果全是模拟还有一堆没学的其他玩意 (回文自动机) 数据结构也没考几次..还是线段树用来辅助搜索......

看看普及 全是水题 当然偶尔还有一点我不会的 (某些算法) =-= 万念俱灰 无奈之下待在了提高组...

于是在老师的强迫我坚毅的信念下开始填坑=-=这里是dp专题

 找找常见dp的类型......普通dp 树形dp 区间dp 状压dp 数位dp 插头dp......一个一个补

废话不多说先放题目

---------------------------------------第二部分---------------------------------------

2.2.2 Subset Sums集合

题目描述

对于从1到N(1 <= n <= 39)的连续整数集合,能划分成两个子集合,且保证每个集合的数字和是相等的。

举个栗子,如果N=3,对于{1,2,3}能划分成两个子集合,他们每个的所有数字和是相等的:

  • {3} and {1,2}

这是唯一一种分法(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)

如果N=7,有四种方法能划分集合{1,2,3,4,5,6,7},每一种分发的子集合各数字和是相等的:

  • {1,6,7} and {2,3,4,5} {注 1+6+7=2+3+4+5}
  • {2,5,7} and {1,3,4,6}
  • {3,4,7} and {1,2,5,6}
  • {1,2,4,7} and {3,5,6}

给出N,你的程序应该输出划分方案总数,如果不存在这样的划分方案,则输出0。程序不能预存结果直接输出。

 

时空范围呢 是1000ms/256MB

 

INPUT FORMAT

输入文件只有一行,且只有一个整数N

SAMPLE INPUT

7

 

OUTPUT FORMAT

输出划分方案总数,如果不存在则输出0。

SAMPLE OUTPUT

4

先来思考一下=-= n范围是1~39 那么死磕算法肯定是不行的 O(2的n次方)

然后仔细分析题目 每个数字必定属于一个集合 要不是这个要不是那个 并且两集合相等 其总和为(n + 1) * n (好像是高斯那啥)

除2即得单个集合的值 然后这集合里面的数是1~n 可选可不选

不就是01背包么=w=

那么对于每一个数 我们可选 也可不选

设dp[a][b]为 取前a个数 总和为b的方案数量 ...等等 为什么这么设??

动态规划 我们从小找起 一开始只有一个数 可能的值为 1 或 0

然后第二个数 在这两个值上相加...以此类推

那么a就是一个一个数到n 然后b呢 因为按上面那种推法可能结果很多 但显而易见可能结果绝对小于数的总和

因此以数的总和作为b...好了 初始化第一个数 然后开始推

之后呢 看代码注释吧~

下放代码:

#include 
#include 
#include 
#define ll long long
using namespace std;
ll dp[5 << 4][5 << 7];//坑啊 上限大于2147483647 然后我AC率掉了一截
int main()
{
	int n;
	scanf("%d",&n);//认真仔细地输入
	int all = (n + n * n) >> 1;//这里原本是(n + 1) * n / 2 但为了我高大上的位运算=-= 式子怎么来的不说了 别人高斯小学就发现了
	if (all % 2) {putchar('0'),putchar('\n'); return 0;}//判断总数如果为单数 绝对不可能平分成两整数 直接退出 皆大欢喜
	all = all >> 1;//先除以2 因为我发现总数后面用不到
	dp[1][1] = 1;//预处理1个数的情况
	dp[1][0] = 1;//同上
		for (int a = 2 ; a <= n ; a ++)//判断加上第a个数的情况
			for (int b = 0 ; b <= all ; b ++)//判断原有所有状态 更新加上/不加上a的状态
			if (b < a) dp[a][b] = dp[a - 1][b];//更新不加a达到的数的和的方案数
				  else dp[a][b] = dp[a - 1][b] + dp[a - 1][b - a];//更新加上a达到的数的和的方案数
	printf("%d\n",dp[n][all] / 2);//因为这里两个子集一并求了因此要除2... 
	return 0;
}

 

2.3.2 Cow Pedigrees奶牛家谱

题目描述

农民约翰准备购买一群新奶牛。 在这个新的奶牛群中, 每一个母亲奶牛都生两小奶牛。这些奶牛间的关系可以用二叉树来表示。这些二叉树总共有N个节点(3 <= N < 200)。这些二叉树有如下性质:

  1. 每一个节点的度是0或2。度是这个节点的孩子的数目。
     
  2. 树的高度等于K(1 < K < 100)。高度是从根到任何叶子的最长的路径上的节点的数目; 叶子是指没有孩子的节点。

有多少不同的家谱结构? 如果一个家谱的树结构不同于另一个的, 那么这两个家谱就是不同的。输出可能的家谱树的个数除以9901的余数。

INPUT FORMAT

第1行: 两个空格分开的整数, N和K。

SAMPLE INPUT

5 3

 

OUTPUT FORMAT

第 1 行: 一个整数,表示可能的家谱树的个数除以9901的余数。

SAMPLE OUTPUT

2

OUTPUT DETAILS:

有5个节点,高为3的两个不同的家谱:

      @                  @
 
      / \                   / \
   @  @    和    @   @
   / \                         / \
@ @                    @ @

然后别的博客上有个人解释和USACO的官方解释 这里给个传送门 Tip:还是回来吧 我这里详细得多OvO

分析一下嘛 这题呢

就是求 一定高度 树上有一定节点 分布在树上的方法

然后方法有两种: 子树没节点 或 子树两个节点 然后后者子树下一层的节点也同这两种

设子树为2个 是 X 为0个 是 O 则

该层某地方的上层 可能是XOOXXOXXXOXOOXX.........嘛 太复杂了 我们排除掉纯暴力的想法 然后......DP!

树形DP!!!

从根节点开始推 首先我们知道 根节点只有一种情况——有 因此他肯定有两儿子

嘛 后面的点嘛 就有两种情况 有或没有 后面每层都这样

然后如果要到下一层 那下一层的点肯定有父亲 但他父亲那层的点有多少...我们只知道这种情况下 只有他父亲必然存在

叶子节点必然有父亲

那点就是不确定的了......那我们就要推啊 好 我们设点~ (喂喂怎么能这么爽快我还不清楚情况呢)

题目正好又给了节点个数 3≤N<200 那我们从3开始推 根据节点度数很轻松地能发现 总共点数只可能是奇数 刚好 1 + 2 = 3

那么从3推起刚好能衔接上!!

Tip:如果想读入N时预处理去掉偶数情况也可以 但是我懒...反正后面推导后这些情况的答案也是0

嘛 我们点数22累加 算到N就好

嗯 点可以推到N 那深度呢......好 我们设深度~ (喂喂怎么又是这样子解释清楚啊)

呐 我们要求深度为K 点数为N的情况嘛 然后一开始的情况已经知道了 深度为1 点数为1 情况为1

那就推深度和情况啊 (搞了这么久原来是这个意思...)

嘛 我们深度11累加 算到K就好

这里考虑点数一定时 分布在所有深度的情况

因此这里存的状态是当前节点数 当前深度及以上(浅) 的 所有状态

点数一定 点数分布太多太杂 像上面的XOOXXOXXXOXOOXX.........我们肯定不能对当前节点所在的状态进行推导 动态规划嘛

说了点数一定 那就按当前点数推 点数分布...树叉太多了 怎么办?

取根节点的左右子树!! 而且根据n的范围 这样刚好可以涵盖所有情况 当然 如果 n ≥ 9 理论推测你可以取四棵树

那就设左子树节点...根节点 有 1个左 1个右 然后后面其他节点就只可能0/2

那初始状态为1左(因为没有左自然没有右 根节点没有左边那一个就没有右边那一个)

设当前z总节点个数为a 左节点为cnt(count缩写)个 右节点就有a - cnt - 1 个(1个根节点)

那显而易见 左边 cnt 个 右边 a - cnt - 1 个的情况乘起来 就是总共a个的情况

原因 例如左边 XOOOOO,OXOOOO....很多情况 则右边OXXXXX,XOXXXX...... 总结点相同

但对于左边每一种排列 右边都有很多种不一样的排列 只要节点总和相同 位置可以任意换 然后对于右边也是 因此要乘 不是加!!

简单理解一下: 两个数列 长度均为6 每个数都不同 问你可能取到多少种不同的数的组合......6×6啊 多明显......

然后对于每一种之前的情况 推到下一层只可能有一种 那就是这层某点有两儿子 那么就可以推出下层a个节点的情况——

         dp[a][dep] += dp[cnt][dep - 1] * dp[a - cnt - 1][dep - 1] 下行有对应的解释=-=

该层a个点的情况||上层左边cnt个点的情况||上层右边a-cnt-1个点的情况

此处为什么+=??

其实因为1个点的时候没办法找式子(根上面还有什么呢) 我们初始化了一下=-=

for (int a = 1 ; a <= h ; a ++) dp[1][a] = 1;(别问我高度为什么不用k要用h......)

这里的1 就是推不到下一层的时候的情况——0

dp[a][dep]只会更新一次(看循环就知道了) 此处加的就是以上情况

说白了就是+1...

但for循环初始化是必须的 因为循环如果搜到了上面情况 0 × 0 = 0 再 + 0 = 0 那根本推不下去

这也是初始化之一~~

因此我们可以推到N个点 深度K的情况 此处注意——

求的是深度为K及比他浅的所有情况和

因此要减去深度比K浅的所有情况 [k - 1] 正好包括了那些所有情况

最后还有一个坑点 关于取膜♂取mod的事——

上层取模后的数可能比这层的大!!! 然而并不会大于9901

因此我懒得判情况 直接加上9901 和原来的数一起 然后取模即可

下放代码~

#include 
#include 
#define mod 9901
using namespace std;
int dp[1 << 9][1 << 10];
int main() {
	int n,h;
	scanf("%d%d",&n,&h);
	for (int a = 1 ; a <= h ; a ++) dp[1][a] = 1;//预处理 1个点时所有深度的情况(只可能在深度1有1个点)
	for (int a = 3 ; a <= n ; a += 2)//总节点数循环
	for (int dep = 2 ; dep <= h ; dep ++)//每个节点的深度推导
	for (int cnt = 1 ; cnt < a ; cnt += 2) {//总结点相同时的不同情况 这里左右并不是遍历到终点 因为左右子树互换也算一种情况..
		dp[a][dep] += dp[cnt][dep - 1] * dp[a - cnt - 1][dep - 1];//精华の更新
		dp[a][dep] %= mod;//取模
	}
	printf("%d\n",(dp[n][h] + mod - dp[n][h - 1]) % mod);//坑人的输出
	return 0;
}

 

2.3.4 Money Systems货币系统

题目描述

母牛们不但创建了他们自己的政府而且选择了建立了自己的货币系统(不愧于牛批这个词)。

[In their own rebellious way],他们对货币的数值感到好奇。

传统地,一个货币系统是由1,5,10,20 或 25,50, 和 100的单位面值组成的。

母牛想知道有多少种不同的方法来用货币系统中的货币来构造一个确定的数值。

举例来说, 使用一个货币系统 {1,2,5,10,...}产生 18单位面值的一些可能的方法是:18x1, 9x2, 8x2+2x1, 3x5+2+1,等等其它。

写一个程序来计算有多少种方法用给定的货币系统来构造一定数量的面值。

保证总数将会适合long long (C/C++) 和 Int64 (Free Pascal)。

 

INPUT FORMAT

货币系统中货币的种类数目是 V 。 (1<= V<=25)

要构造的数量钱是 N 。 (1<= N<=10,000)

第 1 行: 二整数, V 和 N
第 2 ..V+1行: 可用的货币 V 个整数 (每行一个 每行没有其它的数)。

SAMPLE INPUT

3 10
1 2 5

 

OUTPUT FORMAT

单独的一行包含那个可能的构造的方案数。

SAMPLE OUTPUT

10

炒鸡经典的啦=-=

感觉很像多重背包 就一个数可以取多次 然后和其他数(或者不和)组合 输出能达到指定value的情况

不对 就是多重背包=-=

那我们考虑每次遍历一个"物品"的价值 因为怕更新到负数的地方 因此从物品价值开始更新

直到指定value 然后更新过程中每次能达到的value

即如果一个value可以通过以前的一个value加上物品价值得到 当前value就加上之前那个value的情况

此处背包存的是到达某价值的情况数量啊 切记=-=

通过把每个物品遍历一次 就可以得到所有的情况了

查询O(1) 然而这种题目怎么可能给你查询多次2333

只用输出第value个就好~(忽略0下标)

注意此处可能结果大于int=-= 用 long long 存......能尽量不用还是不用好 毕竟慢死

下放代码~

#include 
#include 
#define ll long long
using namespace std;
int v[1 << 5];
ll ans[5 << 11] = {1};
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for (int a = 1 ; a <= n ; a ++) scanf("%d",&v[a]);
	for (int a = 1 ; a <= n ; a ++)
		for (int b = v[a] ; b <= m ; b ++)
			ans[b] += ans[b - v[a]];
	printf("%lld\n",ans[m]);
	return 0;
}

 

 

 

---------------------------------------第三部分---------------------------------------

3.1.2 Score Inflation总分

题目描述

学生在我们USACO的竞赛中的得分越多我们越高兴(这就是你出水题的理由??)。

我们试着设计我们的竞赛以便人们能尽可能的多得分,这需要你的帮助。

我们可以从几个种类中选取竞赛的题目,这里的一个"种类"是指一个竞赛题目的集合,解决集合中的题目需要相同多的时间并且能得到相同的分数。

你的任务是写一个程序来告诉USACO的职员,应该从每一个种类中选取多少题目,使得解决题目的总耗时在竞赛规定的时间里并且总分最大。

输入包括竞赛的时间,M(1 <= M <= 10,000)(不要担心,你要到了训练营中才会有长时间的比赛)和N,"种类"的数目1 <= N <= 10,000。

后面的每一行将包括两个整数来描述一个"种类":

第一个整数说明解决这种题目能得的分数(1 <= points <= 10000),第二整数说明解决这种题目所需的时间(1 <= minutes <= 10000)。

你的程序应该确定我们应该从每个"种类"中选多少道题目使得能在竞赛的时间中得到最大的分数。

来自任意的"种类"的题目数目可能任何非负数(0或更多)。

计算可能得到的最大分数。

 

INPUT FORMAT

第 1 行: M, N--竞赛的时间和题目"种类"的数目。
第 2-N+1 行: 两个整数:每个"种类"题目的分数和耗时。

SAMPLE INPUT

300 4
100 60
250 120
120 100
35 20

 

OUTPUT FORMAT

单独的一行包括那个在给定的限制里可能得到的最大的分数。

SAMPLE OUTPUT

605
{从第2个"种类"中选两题第4个"种类"中选三题}

和上题一样 多重背包

这里存的是到达一个容量时的最大价值

懒得优化的我背包后直接搜了=-=下放代码(最大点316ms真是羞耻)

#include 
#include 
#include 
using namespace std;
int ans[5 << 11],w[5 << 11],v[5 << 11];
int main()
{
	int time,m,tot = 0;
	scanf("%d%d",&time,&m);
	for (int a = 1 ; a <= m ; a ++) scanf("%d%d",&v[a],&w[a]);
	for (int a = 1 ; a <= m ; a ++)
		for (int b = w[a] ; b <= time ; b ++)
		ans[b] = max(ans[b],ans[b - w[a]] + v[a]);
	for (int a = 10000 ; a > 0 ; a --)
	if (ans[a]) tot = max(ans[a],tot);
	printf("%d\n",tot);
	return 0;
}

 

3.1.6 Stamps邮票

题目描述

已知一个 N 枚邮票的面值集合(如,{1 分,3 分})和一个上限 K —— 表示信封上能够贴 K 张邮票。计算从 1 到 M 的最大连续可贴出的邮资。

 

例如,假设有 1 分和 3 分的邮票;你最多可以贴 5 张邮票。很容易贴出 1 到 5 分的邮资(用 1 分邮票贴就行了),接下来的邮资也不难:

  • 6 = 3 + 3
  • 7 = 3 + 3 + 1
  • 8 = 3 + 3 + 1 + 1
  • 9 = 3 + 3 + 3
  • 10 = 3 + 3 + 3 + 1
  • 11 = 3 + 3 + 3 + 1 + 1
  • 12 = 3 + 3 + 3 + 3
  • 13 = 3 + 3 + 3 + 3 + 1。

然而,使用 5 枚 1 分或者 3 分的邮票根本不可能贴出 14 分的邮资。因此,对于这两种邮票的集合和上限 K=5,答案是 M=13。

 

INPUT FORMAT

第 1 行: 两个整数,K 和 N。K(1 <= K <= 200)是可用的邮票总数。N(1 <= N <= 50)是邮票面值的数量。
第 2 行 .. 文件末: N 个整数,每行 15 个,列出所有的 N 个邮票的面值,面值不超过 10000。

SAMPLE INPUT

5 2

1 3

 

OUTPUT FORMAT

 

第 1 行:

一个整数,从 1 分开始连续的可用集合中不多于 K 张邮票贴出的邮资数。

SAMPLE OUTPUT

13

 

一看就知道是动规了 感觉也很像......背包??

这里求的是可能得到的数的集合

如果一个个列举肯定会爆 最大值两百万 你还要枚举每个组合 还有好多重复的......

感觉像背包 实际上......应该是吧 给了总的容量 然后自己搭配

因为此处求的是组合个数 就是可能的价值个数 dp数组怎么能存容量呢 

于是dp数组存价值 然后价值一样的时候 容量就可能不一样了

dp是找最优嘛 那么相同价值 容量越小越好 不是吗

那我们因此dp数组初始赋极大值(好吧两百也行) 然后价值为0时容量为0

枚举每个价值 能否通过之前的某种状态 加上这个价值得到当前状态

然后当前状态容量与更新后的比较 如果更小就更新 因为可以装更多 岂不美哉~~

如果装到一个地方容量大于指定的了 说明大包溢出了 直接退出~

(一开始我以为中间可能有断层还开了tot一个个记 结果发现并没有必要 全是连着的啊啊啊)

大概思路就这样 下放代码

#include 
#include 
#include 
using namespace std;
const int MAX = 2000010;
int ans[1 << 21],v[1 << 6],tot;
int main()
{
	int n,m;
	memset(ans,0x7f,sizeof(ans));
	ans[0] = 0;
	scanf("%d%d",&n,&m);
		for (int a = 1 ; a <= m ; ++ a) scanf("%d",&v[a]);
		for (int a = 1 ; a <= MAX ; ++ a)
		{
			for (int b = 1 ; b <= m ; b ++)
				if (a - v[b] < 0) continue; else
				ans[a] = min(ans[a],ans[a - v[b]] + 1);
			if (ans[a] > n)
			{
				printf("%d\n",a - 1);
				break;
			}
		}
	return 0;
}

 

3.2.2 Stringsobits__01串

题目描述

考虑排好序的N(N<=31)位二进制数。

 

你会发现,这很有趣。因为他们是排列好的,而且包含所有可能的长度为N且含有1的个数小于等于L(L<=N)的数。

 

你的任务是输出第I(1<=I<=长度为N的二进制数的个数)大的,长度为N,且含有1的个数小于等于L的那个二进制数。

 

INPUT FORMAT

共一行,用空格分开的三个整数N,L,I。

SAMPLE INPUT

5  3  19

 

OUTPUT FORMAT

共一行,输出满足条件的第I大的二进制数。

SAMPLE OUTPUT

10011

[特典]样例解释---->满足条件的二进制数有(从小到大排列)——

0,1,10,11,100,101,110,111,1000,1001,1010,1011,1100,1101,1110,10000,10001,10010,10011

0也是啊 发现没有 发现没有!!!

 

我天=-=这个动规我愣是没想出来......

好了正题...这题给了你位数 要你求这个位数的所有位数的数的集合里 从小到大排第 i 个 且 含有 1 的数量小于等于 L 的数

分析 每个位数只有两个状态 0 和 1 从第一位开始状态可初始化 我们考虑一维给位数

然后题目要求含有 1 的数量 考虑存不同状态时 1 的 个数 那二维就给他了

状态怎么转移??

首先头肯定是 1 的了 不然......就怪怪的 对 就怪怪的

但是本题动规就是按照有头为 0 来考虑的!!! 怪怪的 对 怪怪的 那为什么呢

因为头为 0 的时候 就可以推到前一个(更少)的位数了啊 显而易见

设推到某处时 i 位数 1 有 j 个 则此时要更新的是 dp[ i ][ j ] (由于本人看见 [j 难受 于是加了空格变成[ j )

我们从上一位数的状态推过来 上一位数的状态则是 dp[ i - 1 ] 那么 j 呢??

按照惯例 肯定是上一位数的所有状态 加上这次更新后的多的状态啦

上一位数的所有状态怎么办呢? 我们把更新后的数分两种情况讨论

第一种 就是继承了之前的所有状态的 但没有新的状态 那怎么得到呢??

前面加 0 !!! 此时存储的状态总数肯定是不变的 (因为数都没变就加了前导 0 ) 但位数变了 就是dp[ i ][ j ] = dp[ i - 1 ][ j ]

第二种 就是这一位数可能为1 此时可能多出好多种情况 但 i 和 j 要和上面的相符

因此 就求上一位的 1 的个数少 1 情况 就是加上这一位的 1 后

1 的总数为 j 的情况 即 dp[ i ][ j ] = dp[ i - 1 ][ j - 1 ]

当然 两者要加起来 就是 dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp[ i - 1 ][ j - 1 ]

这就是递推式啦~~ 然后根据递推式的范围 别忘了预处理哟~

当然本题除了递推式 查询输出也是重点......

首先他位数给出来了呀233 直接暴力加上之前的前缀搜应该也是行的 就从100000000......开始搜 初始化已经得到了dp[ N ][ L ] 然后遇到一个符合的打进去一个 搜到第 i 个退出即可 (仅仅理论 懒死的冰海酱不想打)

但我们考虑比较哲♂学的方法

还是要利用前缀暴力........其实也动了点脑筋的 判断此时第 N 位数是 0 还是 1 这种算法嘛 让吾油来教你好啦——

我们看回递推式 是把当前位数为 0 和 为 1 的 两种情况加起来得到 然后推下去的 然后得到第 i 个数就是答案

那我们能不能以第 i 个数的位置 和他的长度 倒推回去呢 显然可以的 我们知道第 i 个数是第几个 也知道这个位数的第一个数是第几个(就是dp[ i ][ j - 1 ]) 但推到后面可能遇到中间有 00 000 之类的情况 因此 每次更新 如果此时 i ≥ dp[ i ][ j - 1 ] 就说明这个位数是 1 反之是 0 如果是 1 则相应的减去此时的状态数量(dp[ i ][ j - 1 ]) 然后更新( 减 i ) 如果是0就不用减了 直接更新

每次更新记得输出当前位数是 0 还是 1 哦 没必要存数组里了啦~

下放代码(最近比较喜欢缩行2333~)

#include 
#include 
using namespace std;
int dp[1 << 6][1 << 6];
long long n;
int l,k;
int main()
{
	scanf("%d%d%lld",&l,&k,&n);
	for (int a = 0 ; a <= l ; dp[a++][0] = 1);
	for (int a = 0 ; a <= k ; dp[0][a++] = 1);
	for (int a = 1 ; a <= l ; ++ a)
	for (int b = 1 ; b <= l ; ++ b)
	dp[a][b] = dp[a - 1][b] + dp[a - 1][b - 1];
	for (int a = l - 1 ; a >= 0 ; -- a)
	if (dp[a][k] < n) putchar('1'),n -= dp[a][k],--k;
	else putchar('0');putchar('\n');
	return 0;
}

 

3.3.2 Shopping Offers商店购物

题目描述

在商店中,每一种商品都有一个价格(用整数表示)。例如,一朵花的价格是 2 zorkmids (z),而一个花瓶的价格是 5z 。为了吸引更多的顾客,商店举行了促销活动。促销活动把一个或多个商品组合起来降价销售,例如:

  • 三朵花的价格是 5z 而不是 6z,
  • 两个花瓶和一朵花的价格是 10z 而不是 12z。

对于上面的商品信息,购买三朵花和两个花瓶的最少花费是:以优惠价购买两个花瓶和一朵花(10z),以原价购买两朵花(4z)。

编写一个程序,计算顾客购买一定商品的花费,尽量利用优惠使花费最少。尽管有时候添加其他商品可以获得更少的花费,但是你不能这么做。

 

INPUT FORMAT

输入文件包括一些商店提供的优惠信息,接着是购物清单。

 

第一行

优惠商品的种类数(0 <= s <= 99)。
 

第二行..第s+1 行

每一行都用几个整数来表示一种优惠方式。第一个整数 n (1 <= n <= 5),表示这种优惠方式由 n 种商品组成。后面 n 对整数 c 和 k 表示 k (1 <= k <= 5)个编号为 c (1 <= c <= 999)的商品共同构成这种优惠,最后的整数 p 表示这种优惠的优惠价(1 <= p <= 9999)。优惠价总是比原价低。
 

第 s+2 行

这一行有一个整数 b (0 <= b <= 5),表示需要购买 b 种不同的商品。
 

第 s+3 行..第 s+b+2 行

这 b 行中的每一行包括三个整数:c ,k ,和 p 。c 表示唯一的商品编号(1 <= c <= 999),k 表示需要购买的 c 商品的数量(1 <= k <= 5)。p 表示 c 商品的原价(1 <= p <= 999)。最多购买 5*5=25 个商品。

SAMPLE INPUT

2
1 7 3 5
2 7 1 8 2 10
2
7 3 2
8 2 5

 

OUTPUT FORMAT

只有一行,输出一个整数:购买这些物品的最低价格。

SAMPLE OUTPUT

14

 

完全背包=-= 但是好麻烦啊啊啊......

一开始考虑邻接存优惠数组 然后过滤掉不能用的

结果疯狂爆 0 =-= 无奈之下翻标程

没一个是这样的 =-= 突然发现省这么点空间没卵用

然后照搬了下来 =-= 真是羞愧......

这题总共可能买的商品最多五种 好的 那么直接五维数组走起

dp[6][6][6][6][6] 由于下标 0 懒得转换

对于状态转换 5个循环枚举 然后里面套一个可用优惠的循环

现在状态 = min(当前状态 , (现在 - 当前优惠涉及的n种商品 的数量)的状态 + 优惠价)

直接更新 然后有些 0 的......没必要判 直接算进去也是可以的

这题真的是麻烦...... 然后为了压行后的代码更加美观 我又压了一下2333......下放

#include 
#include 
using namespace std;
int dp[6][6][6][6][6],p[105][1005],num[105],orip[105],a[6],b[6];
struct trades {
	int id,k,r;
} t[6];
int main()
{
	int s,n,c,k;
	scanf("%d",&s);
	for (int i = 1 ; i <= s ; ++ i)
	{
		scanf("%d",&num[i]); for (int j = 1 ; j <= num[i] ; ++ j)
		scanf("%d%d",&c,&k),p[i][c] = k;
		scanf("%d",&orip[i]); // 呐 美观吧 我for都压上去了
	}
	scanf("%d",&n); for (int i = 1 ; i <= n ; ++ i)
	scanf("%d%d%d",&t[i].id,&t[i].k,&t[i].r); //呐 美观吧
	for (a[1] = 0; a[1] <= t[1].k ; ++ a[1])
	for (a[2] = 0; a[2] <= t[2].k ; ++ a[2])
	for (a[3] = 0; a[3] <= t[3].k ; ++ a[3])
	for (a[4] = 0; a[4] <= t[4].k ; ++ a[4])
	for (a[5] = 0; a[5] <= t[5].k ; ++ a[5]) //呐 美观吧
	{
		int tot = 0; //为了防止里面某几行太长 这里加了个定义=-=
		for (int i = 1 ; i <= 5 ; ++ i) tot += a[i] * t[i].r;
		for (int i = 1 ; i <= s ; ++ i)
		{
			if (num[i] > n) continue;
			for (int j = 1 ; j <= n ; ++ j) b[j] = a[j] - p[i][t[j].id];
			if (b[1] < 0 || b[2] < 0 || b[3] < 0 || b[4] < 0 || b[5] < 0) continue;
			tot = min(tot,dp[b[1]][b[2]][b[3]][b[4]][b[5]] + orip[i]);//丑死了这四行=-=
		}
		dp[a[1]][a[2]][a[3]][a[4]][a[5]] = tot;
	}
	printf("%d\n",dp[t[1].k][t[2].k][t[3].k][t[4].k][t[5].k]);
	return 0;
}

 

3.3.4 Home on the Range家的范围

题目描述

农民约翰在一片边长是N (2 <= N <= 250)英里的正方形牧场上放牧他的奶牛。

(因为一些原因,他的奶牛只在正方形的牧场上吃草。)

遗憾的是,他的奶牛已经毁坏一些土地。( 一些1平方英里的正方形)

农民约翰需要统计那些可以放牧奶牛的正方形牧场(至少是2x2的,在这些较大的正方形中没有小于1x1的部分被分割毁坏)。

你的工作要在被供应的数据组里面统计所有不同的正方形放牧区域(>2x2)的个数。

当然,放牧区域可能是重叠。

 

INPUT FORMAT

 

第 1 行:

 

  N,牧区的边长。

 

第 2 到 n+1行:

 

N个没有空格分开的字符。

 

0 表示 "那一个区段被毁坏了";1 表示 " 准备好被吃"。

SAMPLE INPUT

6
101111
001111
111111
001111
101101
111001

 

OUTPUT FORMAT

输出那些存在的正方形的大小和个数,一种一行。

SAMPLE OUTPUT

2 10
3 4
4 1

 

直接搜索爆了 QwQ 还剩一个点是这样的 n = 250 而且全是 1 的特殊情况=-=

于是加了个O2(大罪大孽) 784ms过了=-=

但为了弄透我去搜了百度 然后发现......动规

好吧原本就觉得是动规就是懒得打

先放放暴力搜索的方法 (原创但是原创这种题目的解法并不厉害=-=)

并没有任何实际用处 大忙人们请跳过~~

最主要的还是判断重叠的正方形如何叠加 这里我想了好久=-= 先来看个例子(没加颜色很尴尬)

Tip:原图就是读入哒~

【USACO题库】 动态规划 汇总(普及-/普及)_第1张图片

这里为什么要减去呢OwO 因为之前算过的正方形已经包含了

怎么减去呢~~呵 我的o数组用来表示当前点为左上角的正方形的长度的

然后如果当前点更新了 新的o值大于原o值 就加上 怎么加呢

图例(第二个和第三个):o值大了 那大的部分就是没算过的 图中某处由2变成4 那3和4就是新增的

只算3和4 然后推到相邻的 下面和右边的点(原本想直接更新结果发现太麻烦 像上图六个地方重叠)

讲得不详细=-=还是看程序吧 (喂喂你这黑厮..)

#pragma GCC optimize(2)//温馨提示:这句是空的 聪明人都看不见
#include 
#include 
using namespace std;
const int MAX = (1 << 8) - 1;
int th[MAX][MAX],ans[MAX],o[MAX][MAX];
inline void r(int &x)//神奇读入
{
	char q = getchar();
	while (q != '0' && q != '1') q = getchar();
	if (q == '0') x = 0; else x = 1;
}
int pd(int x,int y,int len)
{//第一个for:比较新增的y列的每一个数 如果为0(废土地)就退出
	for (int a = x ; a < x + len ; ++ a) if (!th[a][y + len - 1]) return 0;
	for (int a = y ; a < y + len ; ++ a) if (!th[x + len - 1][a]) return 0;
	return 1;//第二个for:比较新增的x列的每一个数 如果为0(废土地)就退出
}//这里th的第[x+len-1][y+len-1]个判断了两次..其实可以把第二个循环条件改成 y+len-1 然而用处不大
void update(int x,int y,int len)
{
	if (o[x][y] >= len) return;//如果原o值大于等于将修改的边长为len的正方形的值 说明之前的计算已经包含了 直接退出
	for (int a = len ; a > o[x][y] ; ++ans[a--]);//当前计算未包含 加上
	o[x][y] = len;//更新o值
	update(x + 1,y,len - 1);//继续推
	update(x,y + 1,len - 1);//继续推
}
void fs(int x,int y)//原本想打dfs觉得不像,bfs也不像,然后打了fs...就是搜索
{
	int a = 0;//初始化当前正方形边长
	while (pd(x,y,++a));//边长每次加1 然后判断...见那个子程序
	if (--a < 2) return;//因为边长增加到判断到废土地的那一行 要-1 然后边长小于2的不计 两句合并
	update(x,y,a);//正方形左上角及边长已确定 开始更新
}
int main()
{
	int n;
	scanf("%d",&n);
	for (int a = 1 ; a <= n ; ++ a)
	for (int b = 1 ; b <= n ; ++ b) r(th[a][b]);//读入点
	for (int a = 1 ; a < n ; ++ a)
	for (int b = 1 ; b < n ; ++ b) fs(a,b);//以每个点为左上角慢慢搜索更新
	for (int a = 2 ; a <= n ; ++ a) if (ans[a]) printf("%d %d\n",a,ans[a]);//输出答案
	return 0;
}

好了好了正经一点 这是dp专题 我们考虑dp (其实我也不信有人会看我那不开O2不特判一千八ms的东西)

然后我发现那...动规居然和我的有意取童♂工之妙

嗯哼 dp[a][b]表示以该处为右下角 得到的最大的正方形

为了防止世界被破坏被重叠的正方形干扰 我们要取以该点往上和往左能延伸的最小边长 取min

然后每个点遍历一遍 把2到该点值都加一遍到ans中 原理和本人的暴力差不多

好啦大概就是这样 下放拍过的代码~

#include 
#include 
#include 
using namespace std;
const int MAX = (1 << 8) - 1;
int th[MAX][MAX],dp[MAX][MAX],ans[MAX];
inline void r(int &x)
{
	char q = getchar();
	while (q != '0' && q != '1') q = getchar();
	if (q == '0') x = 0; else x = 1;
}
int main()
{
	int n;
	scanf("%d",&n);
	for (int a = 1 ; a <= n ; ++ a)
	for (int b = 1 ; b <= n ; ++ b) r(th[a][b]);
	for (int a = 1 ; a <= n ; ++ a)
	for (int b = 1 ; b <= n ; ++ b)
	if (th[a][b]) dp[a][b] = min(dp[a - 1][b - 1],min(dp[a - 1][b],dp[a][b - 1])) + 1;
	for (int a = 2 ; a <= n ; ++ a)
	for (int b = 2 ; b <= n ; ++ b)
	for (int p = 2 ; p <= dp[a][b] ; ++ p) ++ans[p];
	for (int a = 2 ; a <= n ; ++ a) if (ans[a]) printf("%d %d\n",a,ans[a]);
	return 0;
}

呐 你看 同样的思路 别人dp的就是快 32ms过了 =-= 而且代码又短又好看

 

3.3.5 A Game游戏

题目描述

有如下一个双人游戏:N(2 <= N <= 100)个正整数的序列放在一个游戏平台上,两人轮流从序列的两端取数,取数后该数字被去掉并累加到本玩家的得分中,当数取尽时,游戏结束。以最终得分多者为胜。

 

编一个执行最优策略的程序,最优策略就是使自己能得到在当前情况下最大的可能的总分的策略。你的程序要始终为第二位玩家执行最优策略。

 

INPUT FORMAT

第一行: 正整数N, 表示序列中正整数的个数。
第二行至末尾: 用空格分隔的N个正整数(大小为1-200)。

SAMPLE INPUT


4 7 2 9

5 2

 

OUTPUT FORMAT

只有一行,用空格分隔的两个整数: 依次为玩家一和玩家二最终的得分。

SAMPLE OUTPUT

18 11

 

呐 这篇文章超好的 解释也很详细 看懂了就别回来了

 

(刚开始) 哇 游戏啊 NIM 还是 SG 啊 然而我都没学啊......

(心惊胆战地看完题后) 其实我的内心一直毫无波动 =-= 上一行是骗人的 就是个区间dp而已

然后突然想起 某奶牛零食(洛谷的=-=) 去翻了一下 发现我没过 =-= 不行太羞耻了 打完这部分就去做

区间dp 大都是 取小区间内的最优决策 然后推到大区间 就是从 (1,1) (2,2)....到 (1,2) (2,3)...直到(1,n)

小区间里最优决策初始化 就是只有一个单位的地方 如 (1,1) 初始化很好记 大都这样

然后这题和奶牛零食一类的 都是只能取区间两端的点 那很好推嘛 看看奶牛零食

从 (1,1) 推到 (1,2) 就是 (1,1) + v[2] × 2 和 (2,2) + v[1] × 2 两者的最大值

从 (1,2) 推到 (1,3) 就是 (1,2) + v[3] × 3 和 (2,3) + v[1] × 3 两者的最大值

呐 大概就是这样 但这题呢 是两个人共同取的数列啊=-= 他让你维护第二个最优 然而事实上也要维护第一个最优 =-=

初始化就是寻常的在单点的状态上啦 dp[a][a] = v[a] 这个毋容置疑

然后怎么推上去呢 先看看正(cuo)常(wu)思维

当推到2的时候 他只能选一个 然后他又是先手 那就可以选二者之中最大的

第 1 2 个的dp值都是一个点的最大权值 =-=

然后三个选两个 就某两个里面最大的加上个另外一个

四个选三个 里面三个最大的...加上? 不就成了四选三了吗??嗯 是的 因此不能这么推 =-=

那我们考虑先手拿了这个 后手拿了这个......等等 我们为什么要这么推 这不就是模拟了吗

花了我一个下午终于发现了 (呵 笨死了的Frocean 还四维生物呢) ......dp完全是用来存先手的值的 并没确定哪个是先手 只是题目给了 然后我用了这个条件 从而一直纠结 仅此而已 =-=

设 dp[a][b] 为 a 到 b 的区域内 先手能取得的最大的值

怎么推呢 由题意 易得只有两种状态会转移到该状态 分别是 dp[a+1][b] 和 dp[a][b-1] 为了方便 例子就用第一个吧 =-=

呐 先手在 dp[a][b] 中选了第a个 那后手就是这个区间减去第a个 就是 dp[a+1][b] 的区间中选

然后再之前一个状态 dp[a+1][b] 就是先手啦 求出来了 那个状态之前的状态可能是 dp[a+2][b] 或 dp[a+1][b-1]

然后这个又是后手 但在他减 1 的 区间里面 他又是先手 以此类推

因此 dp[a][b]都存的是当前先手取得的值的 所以算到第 dp[1][n] 的时候 存的就是该测试点的先手取得的值

好了最麻烦的概念清楚了 开始推 (例子依然用上面的)

首先我们可以理所当然地得出后手在这区域内的和 sum - dp[a+1][b] 然后加上点权 v[a] 就是第一张情况

然后第二种情况 就是 sum - dp[a][b-1] 然后加上点权 v[b]

sum 一维的点权前缀和 注意 求 a 到 b ( a ≤ b ) 的区域内 是 sum[b] - sum[a - 1] 因为 sum[a] 包含第a个数 不能减去

然后最优肯定是最大值啦 所以推导式就是——

dp[a][b] = max(sum[b] - sum[a] - dp[a + 1][b] + v[a] , sum[b - 1] - sum[a - 1] - dp[a][b - 1] + v[b])

再次提醒 sum要注意 纵使怪处极多 但我真的没错 =-= 下放代码~

#include 
#include 
using namespace std;
const int MAX = 105;
int dp[MAX][MAX],v[MAX],sum[MAX],n;
int main()
{
	scanf("%d",&n);
	for (int a = 1 ; a <= n ; ++ a) scanf("%d",&v[a]);
	for (int a = 1 ; a <= n ; ++ a) dp[a][a] = v[a];
	for (int a = 1 ; a <= n ; ++ a) sum[a] = sum[a - 1] + v[a];
	for (int len = 1 ; len < n ; ++ len)
	for (int h = 1,t = h + len ; h <= n - len ; ++ h,++ t)
	{
		int p1 = sum[t] - sum[h] - dp[h + 1][t] + v[h];
		int p2 = sum[t - 1] - sum[h - 1] - dp[h][t - 1] + v[t];
		dp[h][t] = max(p1,p2);
	}
	printf("%d %d\n",dp[1][n],sum[n] - dp[1][n]);
	return 0;
}

 

3.4.4 Raucous Rockers“破锣摇滚”乐队

题目描述

你刚刚继承了流行的“破锣摇滚”乐队录制的尚未发表的N(1 <= N <= 20)首歌的版权。你打算从中精选一些歌曲,发行M(1 <= M <= 20)张CD。每一张CD最多可以容纳T(1 <= T <= 20)分钟的音乐,一首歌不能分装在两张CD中。

不巧你是一位古典音乐迷,不懂如何判定这些歌的艺术价值。于是你决定根据以下标准进行选择:

  • 歌曲必须按照创作的时间顺序在CD盘上出现。
  • 选中的歌曲数目尽可能地多。

     

INPUT FORMAT

第一行: 三个整数:N, T, M.
第二行: N个整数,分别表示每首歌的长度,按创作时间顺序排列。

SAMPLE INPUT

4 5 2 4 3 4 2

 

OUTPUT FORMAT

一个整数,表示可以装进M张CD盘的乐曲的最大数目。

SAMPLE OUTPUT

3

 

开始直接暴力 =-= 然而听说可以用dp??

细细想想 这些盘可装可不装 然后装盘有容量......不是很像背包吗

于是就是背包了 =-= 冰海酱爽快地确认道

然后读入的歌的长度当做容积 而且没有权重 (或者为 1 ) 这类背包都是要把物品放最外层的

因此 边读入边更新就好

还有 为了防止一次更新加上许多个该种物品 要倒推 这个看推导式自行脑补

dp存歌曲数量哈 因为题目要求输出这个嘛 然后dp[a][b] 表示第 a 个盘 装了 b (请在此处停顿) 分钟 最大的歌曲数

考虑推导方法——

第一种 就是不加 就是不加 =-= dp[a][b] = dp[a][b]

第二种 加 而且没满 那么 dp[a][b] = dp[a][b - len] + 1 然后 len是歌曲长度 加 1 是歌曲数量

第三种 加 而且满了 那么 dp[a][b] = dp[a - 1][t] + 1 然后 t 就是题目输入的 CD容量上限 因为反正你都装不下了 肯定要取上个碟的最大值 那你肯定懒得判断 直接找上界嘛 反正推的时候会更新到 (这就是背包的好处)

最后懒得判断满没满 反正求最大 便三种一起算 同时为了美(suo)观(hang) 第二种和第三种套一个 max 外面第一种再套个 即可

下放dp代码~~ (暴力太丑就丢掉啦)

#include 
#include 
using namespace std;
int dp[22][22],ans;
int main()
{
	int n,t,m,len;
	scanf("%d%d%d",&n,&t,&m);
	while (n--)
	{
		scanf("%d",&len);
		for (int a = m ; a > 0 ; -- a)
		for (int b = t ; b >= len ; -- b)
		dp[a][b] = max(dp[a][b],max(dp[a - 1][t],dp[a][b - len]) + 1);
	}
	printf("%d\n",dp[m][t]);
	return 0;
}

 

 

 

---------------------------------------第四部分---------------------------------------

4.1.1 Beef McNuggets麦香牛块

题目描述

农夫布朗的奶牛们正在进行斗争,因为它们听说麦当劳正在考虑引进一种新产品:麦香牛块。奶牛们正在想尽一切办法让这种可怕的设想泡汤。奶牛们进行斗争的策略之一是“劣质的包装”。“看,”,奶牛们说,“如果你用只有一次能装3块、6块或10块的三种包装盒装麦香牛块,你就不可能满足想要一次只想买1、2、4、5、7、8、11、14或17块麦香牛块的顾客了。劣质的包装意味着劣质的产品。”

你的任务是帮助这些奶牛。给出包装盒的种类数N(1<=N<=10)和N个代表不同种类包装盒容纳麦香牛块个数的正整数(1<=i<=256),输出顾客不能用上述包装盒(每种盒子数量无限)买到麦香牛块的最大块数。如果在限定范围内所有购买方案都能得到满足,则输出0。

范围限制是所有不超过2,000,000,000的正整数。

 

INPUT FORMAT

第1行: 包装盒的种类数N
第2行到N+1行: 每个种类包装盒容纳麦香牛块的个数

SAMPLE INPUT

3
3
6
10

 

OUTPUT FORMAT

输出文件只有一行数字:顾客不能用包装盒买到麦香牛块的最大块数或0(如果在限定范围内所有购买方案都能得到满足)。

SAMPLE OUTPUT

17

 

做这题气得我想吃麦香牛块

我一个盒子干嘛要装满啊真的是

好了正题 =-=

首先数论知识 两个数 p 和 q 且 gcd(p,q) = 1 (互质)

那么无法表示为两数的 某个倍数 (可以不等) 的 最大的 数 为 p × q - p - q

虽然我也不太懂 =-= 我只知道范围开到 1 << 18 (就是65536 即256 × 256) 多一点就好

然后如果答案大于 1 << 18 答案换成 0

这是个坑点啊啊啊 有个数据 4 252 250 254 256 答案为 0 如果不判的话 答案根据你的范围随机 (神tm)

好了 这题就是个无尽完全背包 不过dp只用存 能不能到达某个容量 初始化 dp[0] = 1 不用解释了吧 =-=

然后更新 我们用 或 ( | ) 即可 就判断两个里面 有一个 1 就更新 然后 1 遇到 0 就不更新

当然用 max 也可以 if 也可以 喜欢就好 但这样最快~~ 下放代码~~

#include 
using namespace std;
const int MAX = 65600;
int dp[MAX + (1 << 4)] = {1},ans;
int main() {
	int n,v;
	scanf("%d",&n);
	for (int a = 1 ; a <= n ; ++ a) {
	scanf("%d",&v);//边读边更新 上一题有 不解释 =-=
		for (int b = v ; b <= MAX ; ++ b)
		dp[b] |= dp[b - v];//或运算
	}
	for (int a = MAX ; a > 0 ; -- a)
		if (!dp[a]) {
		ans = a;
		break;
		}
	if (ans > (1 << 16)) ans = 0;//重要的判断
	printf("%d\n",ans);
	return 0;
}

 

你可能感兴趣的:(解题思路)