目錄
揹包問題
問題:
有 N件物品和一個容量是 V的揹包。每件物品只能使用一次。第 i件物品的體積是 vi,價值是 wi。求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。輸出最大價值。
分析:每次只能選擇一個物品。有4個物品,最大容量是5:2和3加起來剛好不超過揹包最大體積,所以最大爲8.
動態規劃要用兩個方面來表示:
1、狀態表示:需要幾維,f(i, j);包括集合是什麼?屬性(所有選法的最max/min 數量)?揹包問題屬於集合。
集合需要考慮的條件:所有選法中 1)只考慮前i個物品 2)總體積不超過j;得到總價值最大的集合
2、狀態計算
表示集合劃分,將這個集合劃分多個小的集合。
原則:不重複/不漏
比如 從1~i 中選總體積不超過j的最大價值。可以理解爲從1~i-1中選出總體積不超過j的最大價值。==》f(i - 1, j);
/*
f[i][j]:
1.不選第一個物品:f[i][j] = f[i - 1][j];
2.選第i個物品:f[i][j] = f[i - 1][j - v[i]]
f[i][j] = max(1, 2)
f[0][0] = 0;
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品個數,m表示揹包容量
int v[N], w[N];//體積,價值
//暴力做法
int f[N][N];//存所有狀態
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++)
for (int j = 0; j <= m; j ++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
//優化做法:使用滾動數組來做,
//如果f(i)只用到了f(i - 1),縮到一維來做,交替來算。f(0)和f(1)交替來算
// int f[N];//所有狀態
// int main()
// {
// cin >> n >> m;
// for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
// for (int i = 1; i <= n; i ++)
// for (int j = m; j >= v[i]; j --)//枚舉體積
// f[j] = max(f[j], f[j - v[i]] + w[i]);//找最大的
// cout << f[m] << endl;
// return 0;
// }
完全揹包
完全揹包跟揹包問題只有一個區別:每種物品都有無限件可用。有 N種物品和一個容量是V 的揹包,每種物品都有無限件可用。第 i種物品的體積是 vi,價值是 wi。求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。輸出最大價值。
跟揹包問題狀態計算不一樣了,f(i, j)分爲選和不選兩種問題,但是這一次可以無限用。需要劃分無數個子集。先考慮樸素怎麼做,然後在找優化方法。
從1~i-1開始選
總結:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;//n表示物品個數,m表示揹包容量
int v[N], w[N];//體積,價值
//樸素做法
// int f[N][N];//存所有狀態
// int main()
// {
// cin >> n >> m;
// for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
// for (int i = 1; i <= n; i ++)
// for (int j = 0; j <= m; j ++)
// {
// f[i][j] = f[i - 1][j];
// if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
// }
// cout << f[n][m] << endl;
// return 0;
// }
//優化做法
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++)
for (int j = v[i]; j <= m; j ++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
線性DP
數字三角形
問題:給定一個如下圖所示的數字三角形,從頂部出發,在每一結點可以選擇移動至其左下方的結點或移動至其右下方的結點,一直走到底層,要求找出一條路徑,使路徑上的數字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
思路:每一次只能走一個格子,有很多條路可以走。找到一條路徑上所有數字之和最大的。
首先先把他分成i 行j列,用f[i][j]表示所有起點到f[i][j]點所有路徑之和的集合。比如第四行第二個點爲7表示爲(4, 2);從最後的終點往前遞推:如圖劃圈的點7,所以從起點到f[i][j]點路徑分成兩類:一種是從左上方一種是來自右上方。
左上方:比如從7那個點到8,所以需要往上走一格即f[i - 1][j - 1] 並且得加上該點的值a[i][j];
右上方:比如畫圈那個點右上方的1,跟左上方的點8在同一行,但是不同列,所以走一格即f[i - 1, j] + a[i][j];
然後不斷往上遞歸,直到到達起點爲止。最後將兩種情況取max。如下圖所示:
注意邊界問題:如果涉及到i-1的下標循環得從i = 1開始。動態規劃時間複雜度如何求:狀態數量 * 轉移的計算量
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];//表示每個點的值
int f[N][N];//存從起點到第i,j點的路徑最大長度
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= i; j ++)//這裏有問題是<=i,不是<=n
scanf("%d", &a[i][j]);
//初始化,這裏必須注意,從0開始到n,然後每一列得多+1,因爲三角形最右邊有邊界,求f[i][j]的時候會遍歷到每列最右邊的點,然後他的右上角的點實際上不存在的,所以初始化的時候必須把它初始化成INF
for (int i = 0; i <= n; i ++)
for (int j = 0; j <= i + 1; j ++)
f[i][j] = -INF;//這裏得是負無窮
f[1][1] = a[1][1];//第一個點就是他本身的值
//i從2開始
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= i; j ++)
f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);//求左上角右上角那個點的最大值遍歷
//這裏又一個問題,如果是最後一行,則必須得把最後一行的最大值求出來
//最終答案是要遍歷最後一行,最後一行可能會走到每一個位置,求終點的最大值,然後枚舉起點到終點的最大值
int res = -INF;//這裏得是負無窮
for (int i = 1; i <= n; i ++) res = max(res, f[n][i]);
printf("%d\n", res);
return 0;
}
區間DP
石子合併
設有N堆石子排成一排,其編號爲1,2,3,…,N。每堆石子有一定的質量,可以用一個整數來描述,現在要將這N堆石子合併成爲一堆。每次只能合併相鄰的兩堆,合併的代價爲這兩堆石子的質量之和,合併後與這兩堆石子相鄰的石子將和新堆相鄰,合併時由於選擇的順序不同,合併的總代價也不相同。
例如有4堆石子分別爲 1 3 5 2, 我們可以先合併1、2堆,代價爲4,得到4 5 2, 又合併 1,2堆,代價爲9,得到9 2 ,再合併得到11,總代價爲4+9+11=24;如果第二步是先合併2,3堆,則代價爲7,得到4 7,最後一次合併代價爲11,總代價爲4+7+11=22。
問題是:找出一種合理的方法,使總的代價最小,輸出最小代價。
思路:假設有一堆石子,1,3,5,2,我們把1,3合併,5,2合併,總共是4 + 7 + 11 = 22.是最小代價。如下圖所示:
所有合併的個數,如果選取堆數有n - 1次選擇,然後第二次從n- 1中選就有n -2次選擇--》(n - 1)*(n - 2)*.....
狀態表示f[i][j],集合:所有將i到j合併成一堆的方案的集合。(j - i)!。屬性:min,集合中付出的最小代價。
狀態計算:化整爲零的過程,把f[i][j]分解成若干個子問題,分而治之。實際上就是從最後一步開始往前遞推。
最小方案:min(f(i, k)) +min(f(k + 1, j))+從i到j的部分和s[i] - s[i - 1];
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n;
int s[N];//前綴和
int f[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> s[i], s[i] += s[i - 1];//更新前綴和
for (int len = 2; len <= n; len ++)//len從2開始,如果從1開始沒有意義
for (int i = 1; i + len - 1 <= n; i ++)//枚舉區間左端點:i+ len - 1是左邊端點
{
int j = i + len - 1;//枚舉右端點
//枚舉之前
f[i][j] = 1e8;//先將i,J初始化成一個特別大的值
for (int k = i; k < j; k ++)//枚舉k
//式子直接抄過來
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}
//把f[1][n]帶入定義就是所有將1-n合併的方案最大值
cout << f[1][n] << endl;
return 0;
}
計數類DP
整數劃分
https://blog.csdn.net/qq_27262727/article/details/105382085
一個正整數nn可以表示成若干個正整數之和,形如:n=n1+n2+…+nk,其中n1≥n2≥…≥nk,k≥1。我們將這樣的一種表示稱爲正整數n的一種劃分。現在給定一個正整數n,請你求出n共有多少種不同的劃分方法。比如:5,有七種表示方式。
n中的數可以使用無限次,所以可以把它看成完全揹包問題。先回顧一下完全揹包具體做法:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];//存所有狀態
int main()
{
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i ++)
for (int j = i; j <= n; j ++)//j是容量
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
數位統計DP
計數問題
一個正整數nn可以表示成若干個正整數之和,形如:n=n1+n2+…+nkn=n1+n2+…+nk,其中n1≥n2≥…≥nk,k≥1n1≥n2≥…≥nk,k≥1。
我們將這樣的一種表示稱爲正整數n的一種劃分。現在給定一個正整數n,請你求出n共有多少種不同的劃分方法。
思路:暴力做法,出現幾個1就統計幾次,時間複雜度是10^8*8
優化做法:分情況討論,可以轉化爲求1~n中x出現的次數,然後求一個前綴的答案,然後兩個相減
以x = 1爲例,看看怎麼求:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];//存所有狀態
int main()
{
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i ++)
for(int j = i; j <= n; j ++)//j是容量
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
狀態壓縮DP
蒙德里安的夢想
求把N*M的棋盤分割成若干個1*2的的長方形,有多少種方案。例如當N=2,M=4時,共有5種方案。當N=2,M=3時,共有3種方案。
思路:
1、所謂的狀態壓縮DP,就是用二進制數保存狀態。爲什麼不直接用數組記錄呢?因爲用一個二進制數記錄方便作位運算。前面做過的八皇后,八數碼,也用到了狀態壓縮。
2. 本題等價於找到所有橫放 1 X 2 小方格的方案數,因爲所有橫放確定了,那麼豎放方案是唯一的。
3. 用f[i][j]記錄第i列第j個狀態。j狀態位等於1表示上一列有橫放格子,本列有格子捅出來。轉移方程很簡單,本列的每一個狀態都由上列所有“合法”狀態轉移過來f[i][j] += f[i - 1][k]
4. 兩個轉移條件: i 列和 i - 1列同一行不同時捅出來 ; 本列捅出來的狀態j和上列捅出來的狀態k求或,得到上列是否爲奇數空行狀態,奇數空行不轉移。
5. 初始化條件f[0][0] = 1,第0列只能是狀態0,無任何格子捅出來。返回f[m][0]。第m + 1列不能有東西捅出來。
#include<bits/stdc++.h>
using namespace std;
const int N = 12, M = 1 << N;
int st[M];
long long f[N][M];
int main(){
int n, m;
while (cin >> n >> m && (n || m)){
for (int i = 0; i < 1 << n; i ++){
int cnt = 0;
st[i] = true;
for (int j = 0; j < n; j ++)
if (i >> j & 1){
if (cnt & 1) st[i] = false; // cnt 爲當前已經存在多少個連續的0
cnt = 0;
}
else cnt ++;
if (cnt & 1) st[i] = false; // 掃完後要判斷一下最後一段有多少個連續的0
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++)
for (int j = 0; j < 1 << n; j ++)
for (int k = 0; k < 1 << n; k ++)
if ((j & k) == 0 && (st[j | k]))
// j & k == 0 表示 i 列和 i - 1列同一行不同時捅出來
// st[j | k] == 1 表示 在 i 列狀態 j, i - 1 列狀態 k 的情況下是合法的.
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
記憶話搜索
滑雪
給定一個R行C列的矩陣,表示一個矩形網格滑雪場。矩陣中第 i 行第 j 列的點表示滑雪場的第 i 行第 j 列區域的高度。
一個人從滑雪場中的某個區域內出發,每次可以向上下左右任意一個方向滑動一個單位距離。當然,一個人能夠滑動到某相鄰區域的前提是該區域的高度低於自己目前所在區域的高度。
思路:從中間開始滑,從大到小最多能滑25個格子。
狀態表示:
集合:f[i][j]表示狀態,從f[i][j]開始滑,所有表示從i,j開始滑的所有路徑。屬性:max
狀態計算:就是搜索,每個點能不能向上下左右動
下面代碼中的f[x][y] = max(f[x][y],dp(xx,yy)+1);
實際上就是向四個方向判斷之後轉移:
if(a[i-1][j]<now) f[i][j] = max(f[i][j],f[i-1][j]+1);//上
if(a[i+1][j]<now) f[i][j] = max(f[i][j],f[i+1][j]+1);//下
if(a[i][j-1]<now) f[i][j] = max(f[i][j],f[i][j-1]+1);//左
if(a[i][j+1]<now) f[i][j] = max(f[i][j],f[i][j+1]+1);//右
#include<bits/stdc++.h>
#define read(x) scanf("%d",&x)
using namespace std;
const int N = 310;
int n,m,a[N][N],f[N][N];
int dx[4] = {-1,0,1,0};
int dy[4] = {0,1,0,-1};
int dp(int x,int y) {
if(f[x][y]!=0) return f[x][y]; //記憶化的好處
f[x][y] = 1;//初始化
for(register int i=0; i<4; i++) {
int xx = x+dx[i];
int yy = y+dy[i];
if(xx>=1&&xx<=n && yy>=1&&y<=m && a[x][y]>a[xx][yy])
f[x][y] = max(f[x][y],dp(xx,yy)+1);
}
return f[x][y];
}
int main() {
read(n),read(m);
for(register int i=1; i<=n; i++)
for(register int j=1; j<=m; j++)
read(a[i][j]);
int ans = 0;
for(register int i=1; i<=n; i++)
for(register int j=1; j<=m; j++)
ans = max(ans,dp(i,j));
printf("%d\n",ans);
return 0;
}
線性DP
最長上升子序列
給定一個長度爲N的數列,求數值嚴格單調遞增的子序列的長度最長是多少。
思路:如下圖所示數值嚴格單調遞增的子序列最長長度是4.
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++)
{
f[i] = 1;//設f[i]默認爲1,找不到前面數字小於自己的時候就爲1
for (int j = 1; j < i; j ++)
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);// 前一個小於自己的數結尾的最大上升子序列加上自己,即+1
}
int res = 0;
for (int i = 1; i <= n; i ++) res = max(res, f[i]);
cout << res << endl;
}
最長公共子序列
給定兩個長度分別爲N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串長度最長是多少。
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> m >> a + 1 >> b + 1;
for (int i = 1; i <= n; i ++)////要從1開始讀取,因爲會用到i-1
for (int j = 1; j <= m; j ++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
最短編輯距離
給定兩個字符串A和B,現在要將A經過若干操作變爲B,可進行的操作有:
- 刪除–將字符串A中的某個字符刪除。
- 插入–在字符串A的某個位置插入某個字符。
- 替換–將字符串A中的某個字符替換爲另一個字符。
現在請你求出,將A變爲B至少需要進行多少次操作。
1)刪除操作:把a[i]刪掉之後a[1~i]和b[1~j]匹配
所以之前要先做到a[1~(i-1)]和b[1~j]匹配
f[i-1][j] + 1
2)插入操作:插入之後a[i]與b[j]完全匹配,所以插入的就是b[j]
那填之前a[1~i]和b[1~(j-1)]匹配
f[i][j-1] + 1
3)替換操作:把a[i]改成b[j]之後想要a[1~i]與b[1~j]匹配
那麼修改這一位之前,a[1~(i-1)]應該與b[1~(j-1)]匹配
f[i-1][j-1] + 1
但是如果本來a[i]與b[j]這一位上就相等,那麼不用改,即
f[i-1][j-1] + 0
最後f[i][j]就由以上三個可能狀態轉移過來,取個min。
/*
1)刪除操作:把a[i]刪掉之後a[1~i]和b[1~j]匹配
所以之前要先做到a[1~(i-1)]和b[1~j]匹配
f[i-1][j] + 1
2)插入操作:插入之後a[i]與b[j]完全匹配,所以插入的就是b[j]
那填之前a[1~i]和b[1~(j-1)]匹配
f[i][j-1] + 1
3)替換操作:把a[i]改成b[j]之後想要a[1~i]與b[1~j]匹配
那麼修改這一位之前,a[1~(i-1)]應該與b[1~(j-1)]匹配
f[i-1][j-1] + 1
但是如果本來a[i]與b[j]這一位上就相等,那麼不用改,即
f[i-1][j-1] + 0
最後f[i][j]就由以上三個可能狀態轉移過來,取個min
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> a + 1 >> m >> b + 1;
//如果a/b序列爲空則需要增加操作
for (int i = 0; i <= m; i ++) f[0][i] = i;
for (int i = 0; i <= n; i ++) f[i][0] = i;
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//這裏搞錯了是a[i] == b[j],j別看錯了
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[n][m] << endl;
return 0;
}
編輯距離
問題:給定n個長度不超過10的字符串以及m次詢問,每次詢問給出一個字符串和一個操作次數上限。對於每次詢問,請你求出給定的n個字符串中有多少個字符串可以在上限操作次數內經過操作變成詢問給出的字符串。每個對字符串進行的單個字符的插入、刪除或替換算作一次操作。
思路:這題就是上一個求幾遍最短編輯距離就行了。
時間複雜度:1e6 * 10^2 = 1e8,時限兩秒,是ok的
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 15, M = 1001;//N = 15字符串最大長度,M是最大詢問
int n, m, f[N][N];
char s[M][N];
int edit_dis(char a[], char b[])
{
int lena = strlen(a + 1);
int lenb = strlen(b + 1);
for (int i = 1; i <= lena; i ++) f[i][0] = i;
for (int i = 1; i <= lenb; i ++) f[0][i] = i;
for (int i = 1; i <= lena; i ++)
for (int j = 1; j <= lenb; j ++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
//這裏寫錯了是a[i] == b[j]
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
return f[lena][lenb];
}
int main()
{
cin >> n >> m;
//輸入是i < n,不需要<=,因爲i是從0開始的
for (int i = 0; i < n; i ++) cin >> s[i] + 1;
while (m --)
{
char q[N];
int limit;
cin >> q + 1 >> limit;
int ans = 0;
for (int i = 0; i < n; i ++)
if (edit_dis(s[i], q) <= limit) ans ++;
cout << ans << endl;
}
return 0;
}
貪心
區間問題
區間選點
給定N個閉區間[ai,bi],請你在數軸上選擇儘量少的點,使得每個區間內至少包含一個選出的點。輸出選擇的點的最小數量。
位於區間端點上的點也算作區間內。
思路:
當一個數上有點時,包含這個數的區間都會被滿足。因此,我們在推理時,應儘可能“一箭多雕”。
接着,我們的目標就轉化爲“如何儘可能完美地放點”。一個區間,若放較前,則無法顧及後面;若放較後,則無法顧及前面。既然如此,我們就應該有規律地放(從前往後或從後往前,此處講從前往後)。故先要儲存,排序。
要排序,就得有關鍵字。關鍵字分爲二:1.起點、2.終點。若以起點爲關鍵字,我們就不知道點該儘量往哪放。既然從前往後,理應儘量往後放,因爲其他區間都在自己後面。可萬一有一個區間起點在自己之後,終點在自己之前,那麼它就會被巧妙地避開,最後WA。
所以,我們要以終點爲關鍵字。這樣,我們只要將點放在終點的數上就能將儘可能多的區間滿足。步驟如下:
1、輸入;
2、儲存;
3、排序;
4、處理;
5、輸出。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
//結構體排序
struct Range
{
int l, r;
bool operator< (const Range &w)const
{
return r < w.r;
}
}range[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
//枚舉每個區間
for (int i = 0; i < n; i ++)
if (range[i].l > ed)
{
res ++;
ed = range[i].r;
}
cout << res << endl;
return 0;
}
最大不相交區間數量
給定N個閉區間[ai,biai,bi],請你在數軸上選擇若干區間,使得選中的區間之間互不相交(包括端點)。輸出可選取區間的最大數量。
思路:做法跟區間選點操作是一樣的。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
//結構體排序
struct Range
{
int l, r;
bool operator< (const Range &w)const
{
return r < w.r;
}
}range[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
//枚舉每個區間
for (int i = 0; i < n; i ++)
if (range[i].l > ed)
{
res ++;
ed = range[i].r;
}
cout << res << endl;
return 0;
}
區間分組
給定N個閉區間[ai,bi],請你將這些區間分成若干組,使得每組內部的區間兩兩之間(包括端點)沒有交集,並使得組數儘可能小。輸出最小組數。
思路:
這個區間貪心問題,是要按照區間左端點排序。
分情況討論貪心決策:
1.如果一個區間的左端點比當前每一個組的最右端點都要小,那麼意味着要開一個新區間了,這個條件還可以優化成,一個區間左端點比最小組的右端點都要小就開一個新組。
2.如果一個區間的左端點比最小組的右端點大,那麼就放在該組,這其實也是一個貪心,因爲是先考慮最容易放入一個區間的組
這道題對於數據結構上的選擇也要考慮,用一個小頂堆,也就是優先隊列來存儲每一個組的最右端點是最好的數據結構了。
步驟
1.按區間左端點排序
2.掃描所有區間,按以上情況分開處理
3.最後堆中存的所有數據的個數就是組的個數
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
//< 排序
bool operator< (const Range &w)const
{
return l < w.l;
}
}range[N];
int main()
{
//輸入
cin >> n;
for (int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
//排序
sort(range, range + n);
//定義小根堆,定義語法如下
//來維護所有組的最大值
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i ++)
{
//用r來代表區間
auto r = range[i];
//如果堆爲空或者堆頂最小值>=i區間左端點,區間需要開一個新的組
if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
else
{//否則這個區間放在最小值組中
int t = heap.top();
heap.pop();//出堆,把最小值堆頂刪掉刪掉
heap.push(r.r);//加入新的右端點放進去
}
}
cout << heap.size() << endl;//最後輸出組的數量
return 0;
}
區間覆蓋
給定N個閉區間[ai,bi]以及一個線段區間[s,t],請你選擇儘量少的區間,將指定線段區間完全覆蓋。輸出最少區間數,如果無法完全覆蓋則輸出-1。
思路:
分析:令需要覆蓋的區間開頭爲st,結尾爲ed
1.將所有的區間按左端點排序
2.找到能覆蓋st的區間中右端點最大的那一個,從前往後枚舉每個區間,在所有能覆蓋start的區間中,選擇右端點最大的區間,然後將start更新成右端點的最大值
3.更新st,最後判斷ed是否被覆蓋就可以了
證明
在剩下所有能覆蓋start的區間中,選擇右端點最大的區間,則一定會比前面的選擇最優,更快達到end,所以該做法一定是最優。
時間複雜度 O(nlogn)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &w)const
{
return l < w.l;
}
}range[N];
int main()
{
int st, ed;
cin >> st >> ed >> n;
//cin >> n;
for (int i = 0; i < n; i ++)
{
int l, r;
cin >> l >> r;
range[i] = {l, r};
}
sort(range, range + n);
int res = 0;
bool success = false;
for (int i = 0; i < n; i ++)
{
int j = i, r = -2e9;
while (j < n && range[j].l <= st)//找到一個能覆蓋st並且右端點最長的值
{
r = max(r, range[j].r);
j ++;
}
if (r < st)//如果最後找到的值沒有能覆蓋st的就break,如果沒有這一步,遇到全部都是大於st的區間就會TLE
{
res = -1;
break;
}
res ++;
if (r >= ed)//如果st已經大於ed了就break
{
success = true;
break;
}
st = r;//更新st
i = j - 1;
}
if (!success) res = -1;//判斷一下最後覆蓋到的區間是否已經過了ed
cout << res << endl;
return 0;
}
哈夫曼樹
在一個果園裏,達達已經將所有的果子打了下來,而且按果子的不同種類分成了不同的堆。
達達決定把所有的果子合成一堆。
每一次合併,達達可以把兩堆果子合併到一起,消耗的體力等於兩堆果子的重量之和。
可以看出,所有的果子經過n-1次合併之後,就只剩下一堆了。
達達在合併果子時總共消耗的體力等於每次合併所耗體力之和。
因爲還要花大力氣把這些果子搬回家,所以達達在合併果子時要儘可能地節省體力。
假定每個果子重量都爲1,並且已知果子的種類數和每種果子的數目,你的任務是設計出合併的次序方案,使達達耗費的體力最少,並輸出這個最小的體力耗費值。
例如有3種果子,數目依次爲1,2,9。
可以先將1、2堆合併,新堆數目爲3,耗費體力爲3。
接着,將新堆與原先的第三堆合併,又得到新的堆,數目爲12,耗費體力爲12。
所以達達總共耗費體力=3+12=15。
可以證明15爲最小的體力耗費值。
哈夫曼樹:
樹是完全二叉樹,所有葉子結點都是合併的點。
過程: 總和:a根據路徑長度會算三次,同理其他一樣。
方法:每次挑出值最小的來合併。
證明:
1、如果f比b小,但是f比b淺,可以進行交換一下,把最小的點放到最深的地方。意味着第一步就可以合併。
2、n->n-1,n-1的最優解就是n的最優解。當兩個最小值的點合併則變成如下圖所示,把剩下n-1的最小值用f(n-1)這個方案來表示,總代價就是 f(n) = f(n - 1) + a + b;,第一次合併需要a+b的代價。剩下的問題就變成n-1的問題,從n-1的點挑選最小的兩個值合併,往往復復。
//每次求最小值用堆優先隊列來做
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int main()
{
int n;
cin >> n;
//定義小根堆
priority_queue<int, vector<int>, greater<int>> heap;
while (n --)
{
int x;
cin >> x;
//入堆
heap.push(x);
}
int res = 0;//存結果即最小的體力耗費值
while (heap.size() > 1)//只要堆當中元素個數大於1
{
//取出兩個最小的值,進行合併
int a = heap.top();heap.pop();
int b = heap.top();heap.pop();
res += a + b;
heap.push(a + b);//把合併結果入堆
}
cout << res << endl;
return 0;
}
貪心排序不等式
排隊打水
有 n個人排隊到 1 個水龍頭處打水,第 i個人裝滿水桶所需的時間是 ti,請問如何安排他們的打水順序才能使所有人的等待時間之和最小?
思路:
按照從小到大排序,總時間最小
證明:反證法
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int t[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i ++) cin >> t[i];
sort(t, t + n);
LL res = 0;
//這裏注意sort是從小到大排序,最小的應該*最大的值n - 1,所以得倒過來
for (int i = 0; i < n; i ++) res += t[i] * (n - i - 1);
cout << res << endl;
return 0;
}
貪心絕對值不等式
貨倉選址
在一條數軸上有 NN 家商店,它們的座標分別爲 A1A1~ANAN。
現在需要在數軸上建立一家貨倉,每天清晨,從貨倉到每家商店都要運送一車商品。
爲了提高效率,求把貨倉建在何處,可以使得貨倉到每家商店的距離之和最小。
思路:
時間複雜度(O(nlog(n))
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std ;
typedef long long LL ;
const int N = 100010 ;
int q[N] ;
int n ;
int main(){
cin >> n ;
for(int i=1;i<=n;i++){
cin >> q[i] ;
}
sort(q+1,q+1+n) ;
LL res = 0 ;
for(int i=1;i<=n;i++){
res += abs(q[i]-q[(n+1)/2]) ;
}
cout << res << endl ;
return 0 ;
}
貪心推公式
耍雜技的牛
農民約翰的N頭奶牛(編號爲1..N)計劃逃跑並加入馬戲團,爲此它們決定練習表演雜技。
奶牛們不是非常有創意,只提出了一個雜技表演:
疊羅漢,表演時,奶牛們站在彼此的身上,形成一個高高的垂直堆疊。
奶牛們正在試圖找到自己在這個堆疊中應該所處的位置順序。
這N頭奶牛中的每一頭都有着自己的重量Wi以及自己的強壯程度Si。
一頭牛支撐不住的可能性取決於它頭上所有牛的總重量(不包括它自己)減去它的身體強壯程度的值,現在稱該數值爲風險值,風險值越大,這隻牛撐不住的可能性越高。
您的任務是確定奶牛的排序,使得所有奶牛的風險值中的最大值儘可能的小。
思路:
把公共部分去掉
滿足這個等式,危險係數就會降低:
把牛的最大值從小到大排序,然後順便計算一下他的能力值,就能求出結果。
//爲了使風險值的最大值最小,應該將牛按照W+S從小到大的順序從下往上排列。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> sums;
bool cmp(int a, int b)
{
return sums[a] < sums[b];
}
int main()
{
int N, W, S;
cin >> N;
vector<int> Ws(N), Ss(N), ranks(N);
sums.resize(N);
for (int i = 0; i < N; i++) {
cin >> W >> S;
Ws[i] = W;
Ss[i] = S;
sums[i] = W + S;
ranks[i] = i;
}
sort(ranks.begin(), ranks.end(), cmp);
int res = -1000000000;
int sum_W = 0;
for (int i = 0; i < N; i++) {
int cur_id = ranks[i];
res = max(res, sum_W - Ss[cur_id]);
sum_W += Ws[cur_id];
}
cout << res;
return 0;
}
阿里筆試題
1、養雞場問題
小強有n個養雞場,弟i個養雞場初始有a[i]只小雞。與其他養雞場不同的是,他的養雞場每天增加k只小雞,小強每天結束都會在數量最多的養雞場裏賣掉一半的小雞,假如一個養雞場有x只雞,則賣出後只剩下x/2(向下取整)只雞。問m天后小強的n個養雞場一共多少隻小雞?
輸入 第一行輸入三個int類型n,m,k(1 <= n,m,k <= 10^6) 第二行輸入n個正整數,表示n個養雞場初始雞的個數
輸出 輸出一個整數表示雞的總數
示例 輸入:
3 3 100
100 200 400
輸出:
925
思路:優先隊列,時間複雜度O(mlogn)
//步驟:
//1、使用大根堆來存每個雞場雞的數量
//2、先將每個雞常數量入堆,求雞場雞數量的總和
//3、m天增加雞的數量:每天增加k只小雞,將堆的top出隊並賣掉一半即:
/*
t = heap.top() + base;//top是數量最多的養雞場,加上每天增加的雞
//然後賣掉一半的雞
int d = (t + 1) / 2;
heap.pop();//把頂部出隊
heap.emplace(t - d - base);//加入賣掉雞之後剩下的雞
*/
//4、得到m天雞的總數:除了總和之外得加上n個養雞場每天增加的小雞
#include <bits/stdc++.h>//包含了目前c++所包含的所有頭文件
using namespace std;
typedef long long ll;//數據範圍10^9所以需要long long
int main()
{
int n, m, k, t;
ll base(0), sum(0);//定義
cin >> n >> m >> k;
priority_queue<int> heap;//默認大根堆
for (int i = 0; i < n; i ++)
{
cin >> t;
heap.emplace(t);//入堆
sum += t;//求雞的總和
}
for (int i = 0; i < m; i ++)
{
base += k;//每一天增加k只小雞
t = heap.top() + base;//top是數量最多的養雞場,加上每天增加的雞
//然後賣掉一半的雞
int d = (t + 1) / 2;
heap.pop();//把頂部出隊
heap.emplace(t - d - base);//加入賣掉雞之後剩下的雞
sum -= d;//總數減去賣掉的雞的數量
}
cout << base*n + sum << endl;//除了總和之外得加上n個養雞場每天增加的小雞
return 0;
}
2、求序列期望
小強得到了長度爲n的序列,但他只對非常大的數字感興趣,因此隨機選擇這個序列的一個連續子序列,並求這個序列的最大值,請告訴他這個最大值的期望是多少?
輸入 第一行n表示序列長度接下來一行n個數描述這個序列,n大於等於1小於等於1000000,數字保證是正整數且不超過100000 第二行n個數字表示序列的值
輸出 保留6位小數
樣例 輸入:
3
1 2 3
輸出:
2.333333
先得理解求最大值期望是什麼意思?
比如有這一組子序列:{1},{2},{3},{1,2},{2,3},{1,2,3},則有1最大的概率1/6,2最大的概率爲2/6,3最大概率3/6,期望14/6
思路:單調棧 + 動態規劃,時間複雜度O(n) 在序列x中,長度爲1的子序列有n個,長度爲2的子序列有n-1個...長度爲n-1的子序列有2個,長度爲n的子序列有1個,總的序列數:c = n+(n-1)+...+2+1 = n*(n+1)/2 個,每個出現的概率相同;
考慮以x[i]爲結尾的子序列,這些子序列中有兩種情況,一種是最大值爲x[i],兩一種是最大值不爲x[i];最大值不爲x[i]的相當於x[i]沒有加入,可以藉助之前的狀態求解;最大值爲x[i]的情況只需記錄有多少個。
用單調棧的思路,從大到小存放出現的元素,並記錄值對應的index值。
#include <bits/stdc++.h>//包含了目前c++所包含的所有頭文件
using namespace std;
typedef long long ll;//數據範圍10^9所以需要long long
typedef pair<int, int> PII;
const int N = 1000006;
double dp[N];//dp[i]表示前i個數中最大的期望
int main()
{
int n, t;
cin >> n;
ll c = (ll)n * (n + 1) / 2;//總的序列數
dp[0] = 0;
stack<PII> m;//定義單調棧
double res = 0;//最大值的期望總和
for (int i = 0; i < n; i ++)
{
cin >> t;//選擇第t個子序列
//判斷棧不爲空並且講小於t的元素出堆
//單調棧pair (first, second), first爲給定的n個數,second爲first的個數
while(!m.empty() && m.top().first <= t) m.pop();//如果棧不爲空,並且棧頂第一個元素<=t,則出棧
//計算i最大的序列個數,如果爲空則就是當前序列+1,反則爲i-棧頂部的個數
int d = m.empty() ? i + 1 : i - m.top().second;//如果棧爲空則d = i + 1,否則爲i - m.top().second
//求前i+1個數中最大的期望
dp[i + 1] = 1.0 * t * d / c + dp[i + 1 - d];
//期望累加
res += dp[i + 1];
m.emplace(t, i);//存t個連續子序列i個序列,emplace函數在容器中直接構造元素
}
cout << res << endl;
}
快手筆試真題
判斷英文單詞大寫字母用法是否正確
思路:
其實只要記錄有多少個大寫字母即可,在遍歷過程中,如果大寫字母的個數小於正在遍歷的下標,說明不符合題解,既不是連續的出現大寫字母,如 “AaAa” 遍歷到第二個 A 時的情況。
最終判斷是否爲全大寫或只是首字母大寫即可。
class Solution {
public:
bool detectCapitalUse(string word) {
int uc = 0;
for (int i = 0; i < word.size(); i++) {
if (isupper(word[i]) && uc++ < i) {//來判斷字符c是否爲大寫英文字母
return false;
}
}
return uc == word.size() || uc <= 1;
}
};
數字解碼字母
一條報文包含字母A-Z
,使用下面的字母-數字映射進行解碼
-
'A' -> 1
-
'B' -> 2
-
...
-
'Z' -> 26
給一串包含數字的加密報文,求有多少種解碼方式 舉個例子,已知報文"12"
,它可以解碼爲AB(1 2)
,也可以是L (12)
所以解碼方式有2種。
**解題思路:**動態規劃(使用遞歸會超時)
這題有點像跳臺階,最後面那個字符i可以單獨一個,也可以和前面的字符i-1合併到一起,這個有點像最後一個臺階跳一步還是兩步。跳一步的話是在DP[i-1]的基礎上跳,跳兩步的話是在DP[i-2]的基礎上跳,因此跳臺階那題的遞推公式爲DP[i]=DP[i-1]+DP[i-2]。而這題的遞歸公式也是類似的。
舉個栗子:
當前給定的字符串是:“1226”
當前位置:0,則可能的編碼爲1
當前位置:1,則可能的編碼爲1-2,12
當前位置:2,則可能的編碼爲1-2-2,12-2,1-22
當前位置:3,則可能的編碼爲1-2-2-6,12-2-6,12-26,1-22-6,1-2-26(這裏單獨一位就是在位置2的基礎上加上6,合併就是在位置1的基礎上加上26)
解釋:若當前在位置1,則當前位置的字符’2’可以選擇與前面那個字符合並或者自己單獨一位(這就類似於跳兩步和跳一步),如果是自己單獨一位,那麼DP[i]=DP[i-1]。如果是要和前面一位合併,DP[i]=DP[i-2]。若兩種情況都是滿足的,那麼DP[i]=DP[i-1]+DP[i-2]。
在進行DP的時候還需要考慮幾種特殊的情況:
(1)當前位置i的字符爲’0’,那麼不能考慮單獨一位,只能考慮合併
(2)當前位置和前面的位置合併的時候>‘26’,那麼只能考慮單獨一位
class Solution {
public:
int numDecodings(string s) {
if (s.size() == 0)
return 0;
vector<int> decodeNum = vector<int>(s.size() + 1, 0);
decodeNum[0] = 1;
decodeNum[1] = s[0] == '0' ? 0 : 1;
for (int i = 2; i <= s.size(); ++i) {
if (s[i - 1] != '0')
decodeNum[i] = decodeNum[i - 1];
int num = stoi(s.substr(i - 2, 2));
if (num >= 10 && num <= 26)
decodeNum[i] += decodeNum[i - 2];
}
return decodeNum[s.size()];
}
};
簡化文件路徑
給定一個文檔 (Unix-style) 的完全路徑,請進行路徑簡化。
例如,
path = "/home/", => "/home"
path = "/a/./b/../../c/", => "/c"
思路:邊界情況:
- 你是否考慮了 路徑 = "/../" 的情況?
在這種情況下,你需返回 "/" 。
- 此外,路徑中也可能包含多個斜槓 '/' ,如 "/home//foo/" 。
在這種情況下,你可忽略多餘的斜槓,返回 "/home/foo"
這道題的要求是簡化一個Unix風格下的文件的絕對路徑。
字符串處理,".."是返回上級目錄(如果是根目錄則不處理),重複連續出現的'/',只按1個處理, 如果路徑名是".",則不處理;
class Solution {
public:
string simplifyPath(string path)
{
int len = path.size();
string str = "";
stack<string> q;
for(int i = 0; i < len; i++)
{
if(i == len - 1 && path[i] != '/')
str += path[i];
if(path[i] == '/' && str == "")
continue;
else if(path[i] == '/' || i == len - 1)
{
if(str == "..")
{
if(!q.empty())
q.pop();
}
else if(str == ".")
{
}
else
{
q.push(str);
}
str = "";
}
else
str += path[i];
}
string res = "";
while(!q.empty())
{
res = q.top() + res;
res = "/" + res;
q.pop();
}
if(res == "")
return "/";
return res;
}
};
Leetcode 546.移除盒子
給出一些不同顏色的盒子,盒子的顏色由數字表示,即不同的數字表示不同的顏色。你將經過若干輪操作去去掉盒子,直到所有的盒子都去掉爲止。每一輪你可以移除具有相同顏色的連續 k 個盒子(k >= 1),這樣一輪之後你將得到 k*k 個積分。當你將所有盒子都去掉之後,求你能獲得的最大積分和。
解釋:
[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (3*3=9 分)
----> [1, 3, 3, 3, 1] (1*1=1 分)
----> [1, 1] (3*3=9 分)
----> [] (2*2=4 分)
思路
通過用 dp[i][j][k] 來表示通過移除boxes[i, j]中的箱子,且此時在boxes[i]前有k個箱子的顏色與boxes[i]的顏色相同時,可以獲得的最大分數。此時,可以假設boxes數組的長度是n,可以將結果表示爲:dp[0][n - 1][0],而且此時有如下的一些初始狀態:
dp[i][i][k] = (k + 1) * (k + 1)
dp[i][j][k] = 0; //i < j
考慮一般的情況,對於 dp[i][j][k] 而言,考慮如何將其分解成子問題,以通過遞推來求解。
上面說到,dp[i][j][k] 表示的是通過移除boxes[i, j]中的箱子,且此時在boxes[i]前面有k個與boxes[i]顏色相同的箱子。因此,對於第i個箱子,如果將其和前面的k個箱子一起移除,那麼此時可以獲得的分數,可以表示爲:
(k + 1) * (k + 1) + dp[i + 1][j][0]
同時對於第i個箱子,還有其他的方案來移除,即可以將boxes[i, j]中的某一個箱子一起移除,這個箱子可以表示爲boxes[m],此時boxes[m] == boxes[i]。此時可以獲得的分數,可以表示爲:
dp[i + 1][m - 1][0] + dp[m][j][k + 1]
而此時的 dp[i][j][k] 就是這些情況下可以取得的最大值。
因此可以寫出狀態轉移方程如下:
temp1 = (k + 1) * (k + 1) + dp[i + 1][j][0]
temp2 = max(dp[i + 1][m - 1][0] + dp[m][j][k + 1]) //i <= m <= j && boxes[m] == boxes[i]
dp[i][j][k] = max(temp1, temp2)
/*DP[i][j][k]表示i-j且後面有k個與j相同的元素;
兩種情況 BACAA
第一種,將j後面相同的刪除,dp[i][j][k] = dp[i][j-1][0] + (k+1)*(k+1);
第二種,消除中間的部分 BACAA => BAAA + C;這時候就要分斷點討論了
*/
class Solution {
private:
int mem[101][101][101] ;
public:
int removeBoxes(vector<int>& boxes) {
int n = boxes.size();
memset(mem,0,sizeof(mem));
return dfs(boxes,0,n-1,0);
}
int dfs(vector<int>& boxes ,int l,int r,int k){
if(l > r) return 0;
int rr = r;
int kk = k;
while(l < r && boxes[r] == boxes[r-1]){r--;k++;}
if(mem[l][r][k] > 0) return mem[l][r][k];
int &ans = mem[l][r][k];
ans = dfs(boxes ,l,r-1,0) + (k+1)*(k+1);
for(int i = l;i<r ; i++){
if(boxes[i] == boxes[r]){
ans = max(ans,dfs(boxes ,l,i,k+1) + dfs(boxes ,i+1,r-1,0));
}
}
return ans;
}
};