一,简单递归问题
#include"iostream"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
int t,a,b;
cin>>t;
while(t--)
{
cin>>a>>b;
dp[0]=1;
dp[1]=1;
for(int i=2;i<=b;i++)
dp[i]=dp[i-1]+dp[i-2];
cout<<dp[b-a]<<endl;
}
return 0;
}
从1到2: 1→2;
从1到3: 1→2→3,1→3;
从1到4: 1→2→3→4,1→3→4,1→2→4;
从1到5: 1→2→3→4→5,1→2→4→5,1→3-→4→5,1→2→3→5,1→3→5;
从1到6:1→2→3→4→5→6,1→2→3→4→6,1→2→3→5→6,1→2→4→5→6,1→3→4→5→6,1→2→4→6,1→3→4→6,1→3→5→6;
第 n 个的路径为 f(n)=f(n-1)+f(n-2)(斐波那契数列)。
注意dp数组定义为long long,不然会WA。
#include"iostream"
#include"cstring"
using namespace std;
const int maxn=52;
long long dp[maxn];
int main()
{
int n;
while(cin>>n)
{
memset(dp,0,sizeof(dp));
dp[1]=3;
dp[2]=6;
dp[3]=6;
for(int i=4;i<=n;i++)
dp[i]=dp[i-1]+2*dp[i-2];
cout<<dp[n]<<endl;
}
return 0;
}
我们记f[n]表示长度为n的格子涂色共有f[n]种合法涂法。
涂n个格子有两种方法:
方法一:长度为n-1的格子共有f[n-1]种合法涂法,那么我们在可以在他后面加一个格子涂色,前n-1个格子涂色一定合法,说明第1个和第n-1个格子颜色不一样,那么第n个格子的涂色就只能涂一种,
共有f[n-1]种。
方法二:由长度为n-1的格子的不合法涂法来涂n个格子。长度为n-1的不合法涂法一定由n-2的合法涂法而来(在第n-1个涂与第一个格子相同的颜色,有f[n-2]种。那么第n个格子就可以涂与第一个不同的另外两种颜色,所以总共有2f[n-2]种。
所以递推公式为:f[n]=f[n-1]+2f[n-2];(方法一+方法二)。
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
int n;
while(cin>>n)
{
memset(dp,0,sizeof(dp));
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
cout<<dp[n]<<endl;
}
return 0;
}
观察试题可得发现f(1)=1,f(2)=2,f(3)=3,且最后一个骨牌的排列方式有两种:
1.竖排,n格之前就有f(n-1)种排列方式
2.横排,n-1格之前就有f(n-2)种排列方式
得出:f(n)=f(n-1)+f(n-2) (n>=3)
#include"iostream"
#include"algorithm"
#include"cmath"
using namespace std;
const int maxn=50;
long long a[maxn];
long long b[maxn];
void get()
{
a[1]=0;
a[2]=1;
for(int i=3;i<=20;i++)
{
a[i]=(i-1)*(a[i-1]+a[i-2]);
}
}
void num()
{
b[0]=1;
b[1]=1;
for(int i=2;i<=20;i++)
{
b[i]=b[i-1]*i;
}
}
int main()
{
num();
get();
int n,m;
while(cin>>n)
{
while(n--)
{
cin>>m;
printf("%.2lf%%\n",a[m]*100.0/b[m]);
}
}
}
错排公式
F[n]=(n-1)(F[n-1]+F[n-2])
n 个不同元素的一个错排可由下述两个步骤完成:
第一步,“错排” 1 号元素(将 1 号元素排在第 2 至第 n 个位置之一),有 n - 1 种方法。
第二步,“错排”其余 n - 1 个元素,按如下顺序进行。视第一步的结果,若1号元素落在第 k 个位置,第二步就先把 k 号元素“错排”好, k 号元素的不同排法将导致两类不同的情况发生:
1、 k 号元素排在第1个位置,留下的 n - 2 个元素在与它们的编号集相等的位置集上“错排”,有 f(n -2) 种方法;
2、 k 号元素不排第 1 个位置,这时可将第 1 个位置“看成”第 k 个位置(也就是说本来准备放到k位置为元素,可以放到1位置中),于是形成(包括 k 号元素在内的) n - 1 个元素的“错排”,有 f(n - 1) 种方法。据加法原理,完成第二步共有 f(n - 2)+f(n - 1) 种方法。
#include"iostream"
using namespace std;
long long a[21];
void get()
{
a[1]=0;
a[2]=1;
for(int i=3;i<=20;i++)
{
a[i]=(i-1)*(a[i-1]+a[i-2]);
}
}
long long Cn(int n,int m)
{
long long ans=1;
if(m>n/2)
m=n-m;
for(int i=n;i>n-m;i--)
ans=ans*i;
for(int i=m;i>0;i--)
ans=ans/i;
return ans;
}
int main()
{
int n,m,t;
get();
while(cin>>t)
{
while(t--)
{
cin>>n>>m;
cout<<a[m]*Cn(n,m)<<endl;
}
}
return 0;
}
错排加全排。
#include"iostream"
using namespace std;
int main()
{
int n,m;
while (cin>>n)
{
while (n--)
{
cin>>m;
cout<<2*m*m-m+1;
cout<<endl;
}
}
return 0;
}


折线分平面问题 (该笔记经转载,忘记了原链接)
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
int n;
while(cin>>n&&n)
{
memset(dp,0,sizeof(dp));
dp[1]=1;
dp[2]=2;
dp[3]=3;
for(int i=4;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-3];
}
cout<<dp[n]<<endl;
}
return 0;
}
第1年:1头牛
第2年:2头牛
第3年:3头牛
第4年:4头牛
第5年:6头牛
第6年:9头牛
第7年:13头牛
第8年:19头牛
第9年:28头牛
由此可得:
f(n)=n n<=4
f(n)=f(n-1) + f(n-3) n>4
其中,f(n)是当前年的牛数,f(n-1)是前一年的牛,第n年仍在,f(n-3)是前三年那一年的牛,第n年具有生育能力。
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
int n,t;
cin>>t;
while(t--)
{
memset(dp,0,sizeof(dp));
cin>>n;
dp[1]=1;
dp[2]=1;
dp[3]=2;
for(int i=4;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
cout<<dp[n]<<endl;
}
return 0;
}
依旧是找规律的斐波那契数列。
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=110;
long long dp[maxn];
int main()
{
int n,t;
while(cin>>n)
{
memset(dp,0,sizeof(dp));
dp[1]=3;
dp[2]=8;
for(int i=3;i<=n;i++)
{
dp[i]=2*(dp[i-1]+dp[i-2]);
}
cout<<dp[n]<<endl;
}
return 0;
}
由于长度是n的字符串只有’E’,’O’,’F’组成,并且不能有两个’O’相连,所以我们假设一共有M(n)种排列的可能。而这M(n)中可能又可以分为一下两种:
1)最后一个字符是‘O’:
对于这种情况,很显然第n-1个位置不能是’O’,否则会出现两个’O’相邻的情况,所以此时前面n-1个位置有2M(n-2)种可能。而第n个位置此时是’O’,所以此时共有2乘M(n-2)种可能;
2)最后一个字符不是 ‘O’:
对于这种情况,那么对前面n-1个位置没有特殊的要求,所以对于前面n-1个位置有M(n-1)种可能,而第n个位置有两种可能,所以此时共有2乘M(n-2)种可能。
所以表达式为:M(n)=2[M(n-1)+M(n-2)]。
二, 简单DP入门
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int a[105][105],dp[105][105],n;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
scanf("%d",&a[i][j]);
dp[i][j]=-1;
}
}
for(int i=1;i<=n;i++)
{
dp[n][i]=a[n][i];
}
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=i;j++)
{
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
}
}
cout<<dp[1][1]<<endl;
return 0;
}
需要用抽象的方法思考问题:把当前的位置(i,j)看成一个状态,然后定义状态(i,j)的指标函数d(i,j)为从格子(i,j)出发时能得到的最大和(包括(i,j)本身的值)。在这个状态定义下,原问题的解为d(1,1).
从格子(i,j)出发有两种决策,往左下走或者往右下走,应选择d(i+1,j),d(i+1,j+1)中较大的那一个,即
d(i,j)=(i,j)+max{d(i+1,j),d(i+1,j+1)}
HDU 2069
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int n,dp[110][310];
int m[5]={1,5,10,25,50};
int main()
{
while(~scanf("%d",&n))
{
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=0;i<5;i++)
for(int j=1;j<=100;j++)
for(int k=n;k>=m[i];k--)
dp[j][k]+=dp[j-1][k-m[i]];
int ans=0;
for(int i=0;i<=100;i++)
ans+=dp[i][n];
printf("%d\n",ans);
}
return 0;
}
状态转移方程 dp[ j ][ k ] = dp[ j-1 ][ k-m[i] ] + dp[ j ] [ k ];
三,最长上升子序列问题
//二分加贪心
#include"iostream"
#include"cstring"
#include"algorithm"
#include"cmath"
using namespace std;
const int maxn=10001;
int a[maxn],low[maxn];
int main()
{
int n;
while(cin>>n)
{
memset(a,0,sizeof(a));
memset(low,0,sizeof(low));
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
low[1]=a[1];
int cnt=1;
for(int i=2;i<=n;i++)
{
if(a[i]>low[cnt])
low[++cnt]=a[i];
else
{
int l=1,r=cnt;
while(l<=r)
{
int mid=l+r>>1;
if(low[mid]<=a[i])
l=mid+1;
else
r=mid-1;
}
low[l]=a[i];
}
}
cout<<cnt<<endl;
}
return 0;
}
//动态规划求法
/*#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn = 10010, INF = 0x3f3f3f3f;
int a[maxn], f[maxn];
int n,ans = -INF;
int main()
{
scanf("%d", &n);
for(int i=1; i<=n; i++)
{
scanf("%d", &a[i]);
f[i] = 1;
}
for(int i=1; i<=n; i++)
for(int j=1; j<i; j++)
if(a[j] < a[i])
f[i] = max(f[i], f[j]+1);
for(int i=1; i<=n; i++)
ans = max(ans, f[i]);
printf("%d\n", ans);
return 0;
}*/
新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。
那么,怎么维护 low 数组呢?
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
HDU 1087
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
int main()
{
int a[1005],dp[1005],n,i,j,maxn;
while(scanf("%d",&n)&&n)
{
maxn=0;
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++)
scanf("%d",&a[i]);
dp[1]=a[1];
for(i=1;i<=n;i++)
{
for(j=1;j<=i;j++)
{
if(a[i]>a[j])
dp[i]=max(dp[j]+a[i],dp[i]);
}
dp[i]=max(dp[i],a[i]);
}
maxn=dp[1];
for(i=1;i<=n;i++)
maxn=max(dp[i],maxn);
printf("%d\n",maxn);
}
return 0;
}
我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列。直到求前1个数的最长上升子序列,此时LIS当然为1。
题意是有N个数字构成的序列,求最大递增子段和,即递增子序列和的最大值,思路就是定义dp[i],表示以a[i]结尾的最大递增子段和,双重for循环,每次求出以a[i]结尾的最大递增子段和。
HDU 1159
#include"iostream"
#include"cstring"
#include"algorithm"
using namespace std;
const int maxn=1010;
char s1[maxn],s2[maxn];
int dp[maxn][maxn];
int main()
{
while(~scanf("%s %s",s1,s2))
{
memset(dp,0,sizeof(dp));
int len1=strlen(s1);
int len2=strlen(s2);
for(int i=1;i<=len1;i++)
{
for(int j=1;j<=len2;j++)
{
if(s1[i-1]==s2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
printf("%d\n",dp[len1][len2]);
}
return 0;
}
四,各类背包问题
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=1010;
int n,m,dp[maxn];
pair <int,int> pp[maxn];
int main()
{
int t;
cin>>t;
while(t--)
{
memset(dp,0,sizeof(dp));
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&pp[i].first);
}
for(int i=1;i<=n;i++)
{
scanf("%d",&pp[i].second);
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=pp[i].second;j--)
{
dp[j]=max(dp[j],dp[j-pp[i].second]+pp[i].first);
}
}
cout<<dp[m]<<endl;
}
return 0;
}
01背包模板
状态转移方程:
定义f[i][j]:前i个物品,背包容量j下的最优解
1)当前背包容量不够(j < w[i]),为前i-1个物品最优解:f[i][j] = f[i-1][j]
2)当前背包容量够,判断选与不选第i个物品
选:f[i][j] = f[i-1][j-w[i]] + v[i]
不选:f[i][j] = f[i-1][j]
状态转移方程为:f[j] = max(f[j], f[j-w[i]] + v[i])。
本题我开了一个pair加一维数组,也可以用上述的二维数组AC;
acwing 3
#include"iostream"
#include"algorithm"
#include"cstring"
using namespace std;
const int maxn=1005;
int n,dp[maxn];
pair <int,int> pp[1005];
int main()
{
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d %d",&pp[i].first,&pp[i].second);
}
for(int i=1;i<=n;i++)
{
for(int j=pp[i].first;j<=m;j++)
{
dp[j]=max(dp[j],dp[j-pp[i].first]+pp[i].second);
}
}
cout<<dp[m]<<endl;
return 0;
}
完全背包问题。
本文深入解析了一系列经典算法问题,包括递归、动态规划、最长上升子序列及背包问题,通过具体实例展示了算法的设计思路与实现过程。

1万+

被折叠的 条评论
为什么被折叠?



