??计算机的本质是一个状态机,内存里存储的所有数据构成了当前的状态,CPU只能利用当前的状态计算出下一个状态(不要纠结硬盘之类的外部存储,就算考虑他们也只是扩大了状态的存储容量而已,并不能改变下一个状态只能从当前状态计算出来这一条铁律)
??当你企图使用计算机解决一个问题是,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据)以及如何在状态中转移(怎样根据一些变量计算出另一些变量)。所以所谓的空间复杂度就是为了支持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步!
??例:计算非波那契数,每一个非波那契数就是这个问题的一个状态,每求一个新数字只需要之前的两个状态。所以同一个时刻,最多只需要保存两个状态,空间复杂度就是常数;每计算一个新状态所需要的时间也是常数且状态是线性递增的,所以时间复杂度也是线性的。这种状态计算很直接,只需要依照固定的模式从旧状态计算出新状态就行a[i]=a[i-1]+a[i-2]
,不需要考虑是不是需要更多的状态,也不需要选择哪些旧状态来计算新状态。对于这样的解法,我们叫递推。
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
??无论是DP还是暴力,我们的算法都是在可能解空间内,寻找最优解。暴力做法是枚举所有的可能解,这是最大的可能解空间。DP是枚举有希望成为答案的解(最优子结构)。这个空间比暴力的小得多。也就是说:DP自带剪枝。舍弃了一大堆不可能成为最优解的答案。从而我们可以得到DP的核心思想:尽量缩小可能解空间。在暴力算法中,可能解空间往往是指数级的大小;如果我们采用DP,那么有可能把解空间的大小降到多项式级。一般来说,解空间越小,寻找解就越快。这样就完成了优化。
首先,把我们面对的局面表示为x。这一步称为设计状态。对于状态x,记我们要求出的答案(e.g. 最小费用)为f(x).我们的目标是求出f(T).找出f(x)与哪些局面有关(记为p),写出一个式子(称为状态转移方程),通过f(p)来推出f(x).
??设计DP算法,往往是三个步骤:
??设计状态是DP的基础。接下来的设计转移,有两种方式:一种是考虑我从哪里来;另一种是考虑我到哪里去,这常见于求出f(x)之后,更新能从x走到的一些解。这种DP也是不少的,我们以后会遇到。总而言之,“我从哪里来”和“我要到哪里去”只需要考虑清楚其中一个,就能设计出状态转移方程,从而写代码求解问题。前者又称pull型的转移,后者又称push型的转移。
/*
题意:求n个元素的数组的m个连续子段的和的最大值
dp[i][j] = 选择i个连续子段时 前j个数的最大值
状态转移方程:max[i-1][j-1]=max{ dp[i - 1][k] }(i - 1 <= k <= j - 1)
dp[i][j] = max{dp[i][j - 1],max[i-1][j-1]} + a[j]
解释:第j个元素可以和前面的连接在一起,构成i段dp[i][j-1]+a[j],
或者独自成为一段,dp[i-1][k]+a[j] 因为分成i-1段,所以k>=i-1
前j - 1个数必须组合出i - 1段,选择多种情况里的最大值,
注:最后由于数据较大,所以要压缩空间,使用滚动数组
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX 1000005
const int inf = 0x3f3f3f3f;
int t, n, m, x, y, z;
int a[MAX],mx;
int dp[MAX];
int ma[MAX];
int main()
{
while (scanf("%d%d",&m,&n)!=EOF)
{
memset(dp, 0, sizeof(dp));
memset(ma, 0, sizeof(ma));
for (int i = 1; i <= n; i++)
scanf("%d",a+i);
for (int i = 1; i <= m; i++)
{
mx = -inf;
for (int j = i; j <= n; j++)
{
dp[j] = max(dp[j - 1], ma[j - 1]) + a[j];
ma[j-1]= mx; //为下一轮i循环保留max[i-1][j-1]
mx = max(mx,dp[j]); //mx为dp[i][i]到dp[i][j]的最大值
}
}
printf("%d\n", mx); //因为循环到n所以mx最终为结果
}
return 0;
}
/*
题意:求奇数个元素的数组的出现次数大于一半的数
此数出现的次数大于其他数出现次数的总和,
状态转移方程:新增数==现有最多次数的数,次数+1,else 次数-1
次数为0时转移到新增数
解释:每增加一个数做一次决策判断之前的数是否是新增状态中的次数过半的数,
若判定越过的数当前不可能次数过半,由于一定有次数过半的数,
因此可以暂定新增数为次数过半的数
注:此题没有卡内存,因此还可以用桶装,输出第一个次数过(n+1)/2的数
for (int i = 1; i <= n; i++)
{
cin >> x;
a[x]++;
if (a[x] == (n + 1) / 2)
y = x;
}
cout << y << endl;
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX int(1e6+5)
const int inf = 0x3f3f3f3f;
int t, n, m, cnt, x, y, z;
int a[MAX];
int dp[MAX];
int ma[MAX];
int main()
{
while (cin >> n)
{
cnt = 0;
for (int i = 1; i <= n; i++)
{
cin >> x;
if (cnt == 0)
{
m = x;
cnt = 1;
}
else
{
if (m == x)
cnt++;
else cnt--;
}
}
cout << m << endl;
}
return 0;
}
最长上升子序列(LIS)
/*
**题意:求n个元素的数组中最长的由依次增大的元素组成的子序列
**状态转移方程:if (a[i] > a[j])
** dp[i] = max(dp[i], dp[j] + 1);
** sum = max(dp[i], sum);
**解释:整体最长上升子序列的从头开始的每个部分都是特点末元素的最长上升子序列
** 每增加一个元素,判断以此元素是否比以其他元素大
** 若大则以j元素结尾的最长上升子序列dp[j]+1
** 且得到所有以i元素结尾的子序列,比较得以i元素结尾的最长上升子序列
** 一直到第n个元素,得到以n元素结尾的最长上升序列
** 比较所有元素结尾的最大子序列,得到最长上升子序列
** 注: if (a[i] > a[j])
** dp[i] = max(dp[i], dp[j] + a[i]);
** sum = max(dp[i], sum);
** 此方程得到最大上升子序列(所有元素的和最大)
*/
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
using namespace std;
#define ull unsigned long long
#define ll long long
#define MAX int(1e6+5)
const int inf = 0x3f3f3f3f;
int t, n, m, cnt, sum, flag;
int x, y, z;
int a[MAX];
int dp[MAX];
int main()
{
while (cin >> n)
{
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++)
cin >> a[i];
a[0] = 0;
sum = 0;
for(int i=1;i<=n;i++)
for (int j = 0; j < i; j++)
{
if (a[i] > a[j])
dp[i] = max(dp[i], dp[j] + 1);
sum = max(dp[i], sum);
}
cout << sum << endl;
}
return 0;
}
p[i][j]
来表示第一个串的前i位,第二个串的前j位的LCS的长度,那么我们是很容易想到状态转移方程的:如果当前的A1[i]
和A2[j]
相同(即是有新的公共元素) 那么dp[i][j]=max(dp[i][j],dp[i?1][j?1]+1);
如果不相同,即无法更新公共元素,考虑继承:dp[i][j]=max(dp[i?1][j],dp[i][j?1]
。但对于此题朴素算法是\(n^2\)会因为\(10^5\)规模数据超过时间范围,因此套考虑nlogn算法,因为两个序列都是1~n的全排列,那么两个序列元素互异且相同,也就是说只是位置不同罢了,那么我们通过一个数组将A序列的数字在B序列中的位置表示出来——因为最长公共子序列是按位向后比对的,所以a序列每个元素在b序列中的位置如果递增,就说明b中的这个数在a中的这个数整体位置偏后,可以考虑纳入LCS——那么就可以转变成nlogn求用来记录新的位置的map数组中的LIS。n = 0;
cin >> n;
for (i = 0; i < n; ++i)
cin >> a1[i];
for (i = 0; i < n; ++i)
cin >> a2[i];
for (i = 0; i < n; ++i)
dp[a1[i]] = i; //将a1数组中的数的顺序载入
int len = 0;
for (i = 1; i <n; ++i)
{
if (dp[a2[len]] < dp[a2[i]])
a2[++len] = a2[i];
else //二分查找
{
int j = 0, k = len,mid;
while(j<k)
{
mid = (j + k) / 2;
if (dp[a2[mid]] > dp[a2[i]])
k = mid;
else j = mid + 1;
}
if(dp[a2[j]]>dp[a2[i]])
a2[j] = a2[i];
}
}
cout << len+1<< endl;
int a[MAX],a2[MAX],dp[2][MAX];
n = 0;
while ((cin >> a[n]))
++n;
dp[0][0] = a[0];
dp[1][0] = a[0];
int len1 = 0, len2 = 0;
for (i = 1; i <n; ++i)
{
if (dp[0][len1] >= a[i])
dp[0][++len1] = a[i];
else
{
int p= upper_bound(dp[0], dp[0]+ len1, a[i], greater<int>()) - dp[0];
dp[0][p] = a[i];
}//最长单调不升序列
if (dp[1][len2] < a[i])
dp[1][++len2] = a[i];
else
{
int p= lower_bound(dp[1], dp[1]+ len2, a[i]) - dp[1];
dp[1][p] = a[i];
}//最长单调递减序列
}
cout << len1 + 1 << '\n' << len2 + 1 << endl;
struct node
{
int n, s;
}a[2*MAX+10];
bool cmp(struct node x, struct node y)
{
return x.n < y.n;
}
int main()
{
cin >> n;
for (i = 1; i <= n; ++i)
scanf("%d%d", &a[i].n,&a[i].s);
int len= 1;
sort(a + 1, a + n + 1,cmp);//排序
for (i = 2; i <= n; ++i)//二分求最长单增子序列
{
if (a[i].s > a[len].s)
{
a[++len].s = a[i].s;
}
else
{
j = 1, k = len;
int mid;
while(j<k)
{
mid = (j + k) / 2;
if (a[mid].s > a[i].s)
k = mid;
else j = mid + 1;
}
a[j].s = min(a[j].s, a[i].s);
}
}
printf("%d", len);
return 0;
}
#include <iostream>
#include <cstdio>
using namespace std;
int n,i,j;
int a[1200],f1[1200],f2[1200],maxn;
int main()
{
scanf("%d",&n);
for (i=1;i<=n;i++)
scanf("%d",&a[i]);
for (i=1;i<=n;i++)
f1[i]=f2[i]=1;
for (i=1;i<=n;i++)
for (j=1;j<i;j++)
if (a[i]>a[j])
f1[i]=max(f1[i],f1[j]+1);//求首元素开始到某一元素结束的最长递增序列长度
for (i=n;i>=1;i--)
for (j=n;j>i;j--)
if (a[i]>a[j])
f2[i]=max(f2[i],f2[j]+1);//求某一元素开始到尾元素的最长递减序列长度
for (i=1;i<=n;i++)
maxn=max(maxn,f1[i]+f2[i]-1); //遍历求除最大值
printf("%d\n",maxn);
return 0;
}
f[i]
为以第i个节点结束的最大值,则f[i]=max{f[j]}+a[i] (g[j][i]=1)
,void print(int x)//打印前驱节点
{
if (pre[x] == 0)//起始节点无前驱节点
{
printf("%d", x);
return;
}
else print(pre[x]);
printf(" %d", x);
}
int main()
{
cin >> n;
for (i = 1; i <= n; ++i)
cin >> a1[i];
for(i=1;i<n;++i)
for (j = i + 1; j <= n; ++j)
cin >> a[i][j];
for (i = 1; i <= n; ++i)
{
for (j = 1; j <= n; ++j)
if (a[j][i] == 1&&dp1[j]>dp1[i])
{
dp1[i] =dp1[j];
pre[i] = j;//i点由j点到达
}
dp1[i] += a1[i];
if (dp1[i] > sum)
{
sum = dp1[i];
k = i;//记录结束的节点
}
}
print(k);
cout << '\n'<< sum << endl;
}
w[i][j]
表示第i公司有j台机器的盈利转移方程就是f(i,j)=max(k∈[0,j]){f(i+1,j-k)+w[i][j]}
重点在如何优化数组大小(虽然这题数据极小),注意到转移方程里只用到了f(i+1,l)(l∈[0,j]),因此只要从大到小枚举j就可以在dp数组里去掉i这一维int main()
{
cin >> n>>m;
sum = 0;
for (i = 1; i <= n; ++i)
for (j = 1; j <= m; ++j)
scanf("%d", &a[i][j]);
dp1[0] = 0;
for (i = n; i > 0; --i)
for (j = m; j >=0; --j)
for (k = 1; k <= j; ++k)
if (dp1[j-k] + a[i][k] > dp1[j])
{
dp1[j] = dp1[j-k] + a[i][k];
a1[i][j] = k;
}
cout << dp1[m];
for (i = 1, j = m; i <= n; ++i)
{
cout << endl << i << " " << a1[i][j];
j -= a1[i][j];
}
return 0;
}
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”f[i][0..V]
的所有值。若只用一个数组f[0..V]
,需要保证第i次循环结束后f[v]
中表示的就是我们定义的状态f[i][v]
,因f[i][v]
是由f[i-1][v]
和f[i-1][v-c[i]]
两个子问题递推而来,保证在推f[i][v]
时(也即在第i次主循环中推f[v]
时)能够得到f[i-1][v]
和f[i-1][v-c[i]]
的值即可。事实上,每次主循环中我们以v=V..0的顺序推f[v]
,这样就能保证推f[v]
时f[v-c[i]]
保存的是状态f[i-1][v-c[i]]
的值。scanf("%d%d", &m, &n);
for (i = 1; i <= n; ++i)
scanf("%d%d", a1 + i, a2+i);
for (i = 0; i <= m; ++i)
dp[i] = 0;
for (i = 1; i <= n; ++i)
for (j =m ; j >= 0; --j)
if (a1[i]<=j)
dp[j] = max(dp[j], dp[j-a1[i]] + a2[i]);
printf("%d\n",dp[m]);
洛谷 P2871 [USACO07DEC]手链Charm Bracelet
scanf("%d%d", &n, &m);
for (i = 1; i <= n; ++i)
scanf("%d%d", a[0] + i, a[1] + i);
for (i = 0; i <= n; ++i)
for (j = m; j >= a[0][i]; --j)
dp[j] = max(dp[j - a[0][i]] + a[1][i], dp[j]);
printf("%d\n",dp[m]);
数字组合 OpenJ_Bailian - 4004
dp1[0] = 1;\\初始化和为0的方式为1
for (i = 0; i < n; ++i)
for (j = m; j >= 1; --j)
if (a1[i] <= j && dp1[j - a1[i]] != 0)
dp1[j] += dp1[j - a1[i]];\\得到和为j的组合方式
printf("%d\n", dp1[m]);
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
。f[i][v]
可以由f[i][v-c[i]]+w[i]
得到for (i = 1; i <= n; ++i)
scanf("%d%d", a[0] + i, a[1] + i);
for (i = 1; i <= n; ++i)
for (j = a[0][i]; j<= m; ++j)\\从a[0][i]开始
dp[j] = max(dp[j], dp[j- a[0][i]] + a[1][i]);
int dp[1100][1100];
int MAX;
int main()
{
int i,j,n,m,T,x,y;
scanf("%d",&T);
while(T--)
{
scanf("%d %d %d %d",&n,&m,&x,&y);
MAX=0;
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
{
scanf("%d",&dp[i][j]);
dp[i][j]+=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1];//状态转移方程
if(i>=x&&j>=y) //矩阵大小为x*y
{
MAX=max(MAX,dp[i][j]-dp[i][j-y]-dp[i-x][j]+dp[i-x][j-y]);
}
}
printf("%d\n",MAX);
}
return 0;
}
cin >> t;
while (t--)
{
cin >> m >> n;
sum = 0;
memset(a, 0, sizeof(a));
for (j = 1; j <= m; ++j)
for (i = 1; i <= n; ++i)
{
cin >> a[j][i];
a[j][i] += max(a[j - 1][i], a[j][i - 1]);//只能由左方或上方走到
}
cout << a[m][n] << endl;
}
原文:https://www.cnblogs.com/luanma-2333/p/11257110.html