[SOCI2005]最大子矩阵(DP) + [JXOI2018]守卫(DP) + [CQOI2016]手机号码(数位DP)[各种DP专练]
DP專練博客
DP專練
- T1:最大子矩陣
- 題目
- 題解
- 代碼實現
- T2:守衛
- 題目
- 題解
- 代碼實現
- T3:手機號碼
- 題目
- 題解
- 代碼實現
T1:最大子矩陣
題目
這里有一個n*m的矩陣,請你選出其中k個子矩陣,使得這個k個子矩陣分值之和最大。注意:選出的k個子矩陣不能相互重疊。
輸入格式
第一行為n,m,k(1≤n≤100,1≤m≤2,1≤k≤10),接下來n行描述矩陣每行中的每個元素的分值(每個元素的分值的絕對值不超過32767)。
輸出格式
只有一行為k個子矩陣分值之和最大為多少。
輸入輸出樣例
輸入
3 2 2
1 -3
2 3
-2 3
輸出
9
題解
首先看到這個小的數據范圍(尤其是m),就不明所以然地心動。。。
我們先來考慮m=1的情況
這個問題就轉化為了求一個區間的k個互不重復的線段的和的最大值
肯定都會做DP[i][j]DP[i][j]DP[i][j]:表示到i位置為止一共已經選了j個區間的合法最大值
狀態轉移方程如下:
DP[i][j]=max(DP[last][j?1]+val[i],DP[last][j])(0≤last<i)DP[i][j]=max(DP[last][j-1]+val[i],DP[last][j])(0≤last<i)DP[i][j]=max(DP[last][j?1]+val[i],DP[last][j])(0≤last<i)
那么受m=1的影響啟發,雖然矩陣變成了二維,也不會變得過于復雜
設DP[i][j][k]DP[i][j][k]DP[i][j][k]:表示第一列已經枚舉到了i位,第二列已經枚舉到了j位,一共選了k個矩陣
但是可以發現當i==ji==ji==j的時候這個狀態可以由i和j同時以寬度為2進行轉移,所以會多一個轉移方程式
綜上,轉移方程式如下:
DP[i][j][k]={max(DP[lasti][j][k?1]+val[i][j],DP[i][lastj][k?1]+a[i][j])val就是從last到i為止這個寬度為2的子矩陣值的和:DP[i][j][k]=max(DP[last][last][k?1]+val)(i==j)別忘了還可以不選這個點:DP[i][j][k]=max(DP[last][j][k],DP[i][last][k])DP[i][j][k]= \left\{ \begin{array}{rcl} max(DP[lasti][j][k-1]+val[i][j],DP[i][lastj][k-1]+a[i][j])\\ val就是從last到i為止這個寬度為2的子矩陣值的和:\\ DP[i][j][k]=max(DP[last][last][k-1]+val)(i==j)\\ 別忘了還可以不選這個點:\\ DP[i][j][k]=max(DP[last][j][k],DP[i][last][k])\end{array} \right. DP[i][j][k]=????????????max(DP[lasti][j][k?1]+val[i][j],DP[i][lastj][k?1]+a[i][j])val就是從last到i為止這個寬度為2的子矩陣值的和:DP[i][j][k]=max(DP[last][last][k?1]+val)(i==j)別忘了還可以不選這個點:DP[i][j][k]=max(DP[last][j][k],DP[i][last][k])?
最后分享如何計算這個子矩陣的值
我們可以定義一個二維數組sum[i][3]第二維表示是第幾列,i表示本列到第i位時的值
這樣val就變得很好算了
代碼實現
#include <cstdio> #include <cstring> #include <iostream> using namespace std; int n, m, k; int matrix[105][3], sum[105][3]; int dp[105][105][15]; int main() {scanf ( "%d %d %d", &n, &m, &k );memset ( dp, -0x7f, sizeof ( dp ) );for ( int i = 1;i <= n;i ++ )for ( int j = 1;j <= m;j ++ ) {scanf ( "%d", &matrix[i][j] );sum[i][j] = sum[i - 1][j] + matrix[i][j];}for ( int i = 0;i <= n;i ++ )for ( int j = 0;j <= m;j ++ )dp[i][j][0] = 0;for ( int p = 1;p <= k;p ++ )for ( int i = 1;i <= n;i ++ )for ( int j = 1;j <= n;j ++ ) {dp[i][j][p] = max ( dp[i - 1][j][p], dp[i][j - 1][p] );for ( int last = 0;last < i;last ++ )dp[i][j][p] = max ( dp[i][j][p], dp[last][j][p - 1] + sum[i][1] - sum[last][1] );for ( int last = 0;last < j;last ++ )dp[i][j][p] = max ( dp[i][j][p], dp[i][last][p - 1] + sum[j][2] - sum[last][2] );if ( i == j )for ( int last = 0;last < i;last ++ )dp[i][j][p] = max ( dp[i][j][p], dp[last][last][p - 1] + sum[i][1] - sum[last][1] + sum[j][2] - sum[last][2] );} printf ( "%d", dp[n][n][k] ); return 0; }沒有放表情包真是讓人怪不舒服的,ヾ(?ω?`)o
T2:守衛
題目
九條可憐是一個熱愛運動的女孩子
這一天她去爬山,她的父親為了她的安全,雇了一些保鏢,讓他們固定地呆在在山的某些位置,來實時監視九條可憐,從而保護她。
具體來說,一座山可以描述為一條折線,折線的下方是巖石。這條折線有 n 個折點,每個折點上有一個亭子,第 ii 個折點的坐標是(i,hi) 。九條可憐只可能會在亭子處玩耍,那些保鏢也只會在亭子處監視可憐。
由于技術方面的原因,一個保鏢只能監視所有他能看得到的,橫坐標不超過他所在位置的亭子。我們稱一個保鏢能看到一個亭子 p ,當且僅當他所在的亭子 q 和 p 的連線不經過任何一塊巖石。特別地,如果這條連線恰好經過了除了 p,q 以外的亭子,那么我們認為保鏢看不到可憐。
雇傭保鏢是一件很費錢的事情,可憐的父親希望保鏢越少越好。
可憐的父親還希望得到詳盡的雇傭保鏢的方案,他知道有些亭子可能正在維修,他想對所有的 1≤l≤r≤n 計算:如果事先已知了只有區間 [l,r] 的亭子可以用來玩耍(和監視),那么最少需要多少個保鏢,才能讓[l,r] 中的每一個亭子都被監視到。
可憐的父親已經得到了一個結果,他希望和你核實他的結果是否正確。
輸入格式
第一行輸入一個整數n表示亭子的數目。 接下來一行n個整數,第 i 個整數 hi表示第i個亭子的坐標是(i,hi)
輸出格式
對所有的1≤l≤r≤n 計算:如果事先已知了可憐只會在 [l,r] 這個區間的亭子里面玩耍,那么最少需要多少個保鏢,才能讓 [l,r] 中的每一個亭子都被監視到。由于輸出量太大,可憐的父親只要你輸出所有[l,r]中的每一個亭子都被監視到。由于輸出量太大,可憐的父親只要你輸出所有[l,r]的答案的異或即可。
輸入輸出樣例
輸入
3
2 3 1
輸出
3
說明/提示
樣例解釋
如果r?l+1≤2 ,那么答案顯然是1 。 如果l=1,r=n ,那么答案是 2 ,需要安排兩個保鏢在 (2,3),(3,1) 兩個位置監視可憐。
數據范圍與提示
對于 30% 的數據,n≤20 。
對于 70% 的數據,n≤500 。
對于 100% 的數據,n≤5000,1≤hi≤109
題解
首先通過題意我們知道對于[l,r][l,r][l,r]而言,守衛只能安排在這個區間且只能往前看,他的脖子不允許他轉180
那么r這個點就必須安排一個守衛
觀察這個圖,假設現在守衛在r,那么他能看見的亭子必須夾角得逐步變大
也就是說他的視線在不斷往上移,是無法往下看的,這個可以利用兩點確定一條直線的K值進行判斷
我們可以O(n2)O(n^2)O(n2)預處理出i,j兩個點之間是否可以彼此看見
緊接著不難想到,r看不見的區間是彼此相互獨立的,即
r可以監視的一個點x,那么[x+1,r)的點都不能監視比x更靠前的點
感性理解:x太高了,擋住了后面點的視線,使得他們看不見x前面的點
舉個栗子理解:就是在第二個綠色板塊里面的點無法看見第一個綠色板塊里面的點,
因為第二根黃線的那個點太高了,遮住了第二個綠色板塊點的視線
設DP[l][r]DP[l][r]DP[l][r]表示在[l,r][l,r][l,r]區間中最少要設多少個守衛
知道了當我們固定右端點,那么這個點不能監視到的一些區間[li,ri][li,ri][li,ri]是相互獨立的,
所以想要監視[li,ri][li,ri][li,ri]就必須在ririri或者ri+1ri+1ri+1處設置保鏢,
于是DP轉移方程式
dp[l][r]=1+∑min(dp[li][ri],dp[li][ri+1])dp[l][r]=1+∑min(dp[li][ri],dp[li][ri+1])dp[l][r]=1+∑min(dp[li][ri],dp[li][ri+1])
這樣做是O(n3)的,但是發現對于每一個[li,ri][li,ri][li,ri]區間只與緊鄰的后一個看不見的區間的有瓜葛
我們就可以考慮枚舉右端點,這樣就可以在往前面掃的過程中維護那個∑∑∑,從而復雜度降到了O(n2)
代碼實現
#include <cstdio> #include <cstring> #include <iostream> using namespace std; #define MAXN 5005 int n, result; int h[MAXN]; int dp[MAXN][MAXN]; bool look[MAXN][MAXN]; int main() {scanf ( "%d", &n );for ( int i = 1;i <= n;i ++ )scanf ( "%d", &h[i] );for ( int i = 1;i <= n;i ++ ) {double k = 1e9;for ( int j = i - 1;j;j -- )if ( ( h[i] - h[j] ) * 1.0 / ( i - j ) < k ) {k = ( h[i] - h[j] ) * 1.0 / ( i - j );look[j][i] = 1;}}dp[1][1] = dp[2][2] = dp[1][2] = 1;for ( int i = 3;i <= n;i ++ ) {int sum = 0, last = i;dp[i][i] = 1;for ( int j = i - 1;j;j -- ) {if ( look[j][i] ) {if ( ! look[j + 1][i] )sum += min ( dp[j + 1][last], dp[j + 1][last - 1] );dp[j][i] = sum + 1;last = j;}elsedp[j][i] = min ( dp[j][last - 1], dp[j][last] ) + sum + 1;}}for ( int i = 1;i <= n;i ++ )for ( int j = 1;j <= i;j ++ )result ^= dp[j][i];printf ( "%d", result );return 0; }T3:手機號碼
題目
人們選擇手機號碼時都希望號碼好記、吉利。比如號碼中含有幾位相鄰的相同數字、不含諧音不吉利的數字等。手機運營商在發行新號碼時也會考慮這些因素,從號段中選取含有某些特征的號碼單獨出售。為了便于前期規劃,運營商希望開發一個工具來自動統計號段中滿足特征的號碼數量。
工具需要檢測的號碼特征有兩個:號碼中要出現至少 3 個相鄰的相同數字;號碼中不能同時出現 8 和 4。號碼必須同時包含兩個特征才滿足條件。滿足條件的號碼例如:13000988721、23333333333、14444101000。而不滿足條件的號碼例如:1015400080、10010012022。
手機號碼一定是 11 位數,前不含前導的 0。工具接收兩個數 L 和 R,自動統計出 [L,R]區間內所有滿足條件的號碼數量。L 和 R 也是 11 位的手機號碼。
輸入格式
輸入文件內容只有一行,為空格分隔的 2 個正整數 L,R
輸出格式
輸出文件內容只有一行,為 1 個整數,表示滿足條件的手機號數量。
輸入輸出樣例
輸入
12121284000 12121285550
輸出
5
說明/提示
樣例解釋:滿足條件的號碼: 12121285000、 12121285111、 12121285222、 12121285333、 12121285550。
數據范圍:1010≤L≤R<1011
題解
首先我們可以將[l,r][l,r][l,r]運用差分思想轉化成[1,r]?[1,l?1][1,r]-[1,l-1][1,r]?[1,l?1],這肯定大家都想得到
對于這種位數固定的,且比較小的題目,就可以思考數位DP
設DP[p][last][lastDP[p][last][lastDP[p][last][last_last][flag][limit][flaglast][flag][limit][flaglast][flag][limit][flag_4][flag4][flag4][flag_8]8]8]表示:
模擬到電話號碼的p位時,p+1位的值為last,p+2位的值為last_last
flag表示到當前狀態為止,是否已經出現了至少3 個相鄰的相同數字
flag=1:已經出現;flag=0:還未出現
limit表示到當前狀態為止,前面是否都取的是最大值
limit=1:抵著最大值取的;limit=0:不是抵著最大值取的
eg:12345678910,當p指向3時,limit=1,則表示p+1位取的是2,p+2位取的是1
都是取的當前位能取的最大值
flag_4表示到當前狀態為止,是否出現過數字4
flag_4=1:已經出現;flag_4=0仍未出現
flag_8表示到當前狀態為止,是否出現過數字8
flag_8=1:已經出現;flag_8=0仍未出現
這里要注意幾個點:
1.flag_4和flag_8不能同時為1,在搜索時要特判處理
2.我們由于要把電話號碼值x拆分成各個數位,我們儲存i位的值的num數組是倒著的
所以搜索時就從大到小,這也是為什么上面的例子p+1的電話數位反而在p前面
3.如果搜索到p時,limit是1,那么這個數位的值就被限制了[0,num[p]][0,num[p]][0,num[p]]
否則,這個數位就可以取[0,9][0,9][0,9]
4.可采取記憶化搜索進行剪枝
轉移方程式:
p肯定是p-1,p的上一個就變成了p-1的上上一個,p就變成了p-1的上一個
flag如果先前是1,就不改變,如果在p時剛好p與p+1與p+2相等,就可以變成1,
往下傳,所以這里我們要用∣∣||∣∣
limit如果是1,那么這個位置如果也取的最大值,就往下傳;但如果不是,后面的數位還是可以取[0,9][0,9][0,9],所以這里要用&&\&\&&&
flag_4如果之前出現過,就傳下去;如果當前位是4,就可以賦值成1,所以用∣∣||∣∣
flag_8亦是如此
其實很簡單的,只要會數位DP,可是我不會啊,這就很尷尬了
代碼實現
#include <cstdio> #include <cstring> #define LL long long LL dp[15][15][15][2][2][2][2]; int num[15];LL dfs ( int p, int last, int last_last, bool flag, bool limit, bool flag_4, bool flag_8 ) {if ( flag_4 && flag_8 )return 0;if ( p == 0 )//搜索到末尾了return flag;//flag=1即出現過至少三個是連續的相同數字,這一個電話號碼就是合理的if ( dp[p][last][last_last][flag][limit][flag_4][flag_8] != -1 )return dp[p][last][last_last][flag][limit][flag_4][flag_8];int Limit = limit ? num[p] : 9;LL ans = 0;for ( int i = 0;i <= Limit;i ++ )ans += dfs ( p - 1, i, last, flag || ( i == last && i == last_last ), limit && ( i == Limit ), flag_4 || ( i == 4 ), flag_8 || ( i == 8 ) );return dp[p][last][last_last][flag][limit][flag_4][flag_8] = ans; }LL solve ( LL x ) {if ( x < 1e10 )//特判,如果位數不夠電話號碼的11位return 0;LL result = 0;memset ( dp, -1, sizeof ( dp ) );int len = 0;while ( x ) {num[++ len] = x % 10;x /= 10;}for ( int i = 1;i <= num[len];i ++ )//不能出現前導零result += dfs ( 10, i, 0, 0, ! ( i < num[len] ), i == 4, i == 8 );return result; }int main() {LL l, r;scanf ( "%lld %lld", &l, &r );printf ( "%lld", solve ( r ) - solve ( l - 1 ) );return 0; }總結
以上是生活随笔為你收集整理的[SOCI2005]最大子矩阵(DP) + [JXOI2018]守卫(DP) + [CQOI2016]手机号码(数位DP)[各种DP专练]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鹿鹿鱼鱼是成语吗
- 下一篇: 【莫队/树上莫队/回滚莫队】原理详解及例