【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 <iostream>
#include <cstring>
#include <cstdio>
#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 <iostream>
#include <cstdio>
#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 <iostream>
#include <cstdio>
#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 <iostream>
#include <cstring>
#include <cstdio>
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 <iostream>
#include <cstring>
#include <cstdio>
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 <iostream>
#include <cstdio>
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 <algorithm>
#include <cstdio>
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:原圖就是讀入噠~

這裏爲什麼要減去呢OwO 因爲之前算過的正方形已經包含了

怎麼減去呢~~呵 我的o數組用來表示當前點爲左上角的正方形的長度的

然後如果當前點更新了 新的o值大於原o值 就加上 怎麼加呢

圖例(第二個和第三個):o值大了 那大的部分就是沒算過的 圖中某處由2變成4 那3和4就是新增的

只算3和4 然後推到相鄰的 下面和右邊的點(原本想直接更新結果發現太麻煩 像上圖六個地方重疊)

講得不詳細=-=還是看程序吧 (喂喂你這黑廝..)

#pragma GCC optimize(2)//溫馨提示:這句是空的 聰明人都看不見
#include <cstring>
#include <cstdio>
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 <algorithm>
#include <cstring>
#include <cstdio>
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 <algorithm>
#include <cstdio>
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 <algorithm>
#include <cstdio>
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 <cstdio>
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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章