总体概况
排名25/33相当惨烈的成绩,要不是有签到题,真的要爆零了,这一周dp学下来本以为掌握了一部分,真正上手写题的时候却完全不是那回事:17:34完成F题,之后全在坐牢般地调试G题,心态经过了一系列变化,最终还是没能调试成功。在此提醒朋友们题目一定要自己独立做了才知道会不会,图快看题解是不明智的选择,要有自己的想法再去和题解比对,之后不依赖题解,手搓程序出来,进步会更快。(我的解法也仅供参考,大家要独立思考和体会并模拟那种考试的状态)
比赛采用的是acm赛制,有兴趣可以自己先做一做。
考点包括F贪心的签到,换最少张数的钱Problem - 996A - Codeforces,
H巨大的牛棚[USACO5.3] 巨大的牛棚Big Barn - 洛谷,
B题集邮[USACO3.1] 邮票 Stamps - 洛谷,一道完全背包
G题该死的boredomProblem - 455A - Codeforces,
C题LCISProblem - 977F - Codeforces这几题还是在我的能力范围之内的。
之后的题也给放出来,以后有时间再补题:
D区间dp:BFSProblem - 1114D - Codeforces,
I背包股票:[USACO09FEB] Stock Market G - 洛谷,
A计数类dp:积木[蓝桥杯 2018 国 B] 搭积木 - 洛谷,
E状压dp:[USACO13NOV] No Change G - 洛谷
F:签到题Problem - 996A - Codeforces
本题不需要用到dp的知识,一个贪心足矣:给定一定的钱数和100,20,10,5,1的面额无数张。
最少的张数必然是每次用可用的最大面额去交换,局部最优推整体最优。
虽然赛后讨论时候有人就没想到贪心,以为还是用dp做。
#include<bits/stdc++.h>
using namespace std;//一眼贪心,局部最优
int a = 0,b =0,c = 0,d =0,e = 0,ans = 0;//初始化
int main()
{
int n;
scanf("%d",&n);
while(n>=100){n-=100;e++;}//从最大的面额开始直到1
while(n>=20){n-=20;d++;}
while(n>=10){n-=10;c++;}
while(n>=5){n-=5;b++;}
while(n>0){n-=1;a++;}
ans = a+b+c+d+e;
printf("%d",ans);
return 0;
}
H:巨大的牛棚[USACO5.3] 巨大的牛棚Big Barn - 洛谷
此题的思维方法很巧妙,dp顾名思义是为了解决一个问题,就把该问题分解为多个子问题去解决。
也就是用上一个或多个状态去推导下一个状态。dp这个数组里面有时候存结果,比如本题
我的理解
首先要解决的问题是,如何表示出一个正方形?也就是长和宽相等,并且内部没有树的形状。这是解决本题的关键,dp数组代表的含义更是和它息息相关。
我认为不管做什么dp题,新颖与否,要确定的永远是起点,终点,状态转移方程。
二维数组里,计算机的思维和人的想法是有区别的,人能一眼具象化地看到空缺的位置,但是计算机不同,它要依靠推导,二维前缀和能给到我们一些启示,让我们想到[i][j]元素和[i-1][j]元素,[i][j-1]元素,[i-1][j-1]元素之间的关系,假如我们能正确地给他们赋上意义,找到彼此间的关系,然后从某处起点正确初始化他们,在终点我们一定能找到完美的答案。
灵光乍现,我们想到可以用dp[i][j]表示在以[i][j]处为右下角所能表示出的最大的正方形边长,之所以这样写,我有信心从[1][1]点那边起始,可以顺次历遍所有[i][j]点并存在dp数组里。
状态转移方程:dp[i][j] = min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1//一个小注意点:min只支持两者之间的比较,而谁和谁取min的先后其实不重要,假如三者直接比较会CE(compile error)。
上代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int dp[N][N];
int a[N][N];
int main()
{
int n,t;scanf("%d%d",&n,&t);
int x,y;
for(int i = 0;i<N;i++)
for(int j = 0;j<N;j++)
a[i][j] = 1;//初始化,赋值为1
for(int i = 1;i<=t;i++)
{
scanf("%d%d",&x,&y);
a[x][y] = 0;//有树的点赋0,相当唯一的写法
}
for(int i = 1;i<=n;i++)//历遍数组所有元素
{
for(int j = 1;j<=n;j++)
{
if(a[i][j])
dp[i][j] = min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
}
int ans = 0;
for(int i = 1;i<=n;i++)//再历遍找到最大值
{
for(int j = 1;j<=n;j++)
{
ans = max(ans,dp[i][j]);
}
}
printf("%d",ans);
return 0;
}
注意到,必须初始化没树的点状态为1,有树的点为0,并且从1开始遍历,否则dp[i-1][j]这种会出现dp[-1][0]元素,导致数组越界。
G:Boredom Problem - 455A - Codeforces
题面是英文的,我稍微简化一下:给定一串数字ai(1-n)(含重复的),让你写出所有ak-1和ak+1数字的最大和。以样例三为例:ak=1,就取0(没出现)和2的总和10,ak=2,取1和3的总和8,ak=3,取2和4(没出现)的总和10,最后取最大值10输出。
思路分析
如何去更新dp[i]是我们考虑的问题,这个转移方程它要符合所有的dp[i]最终保留此刻i处的最优解。假如你没有学过动态规划,你可能第一反应是排序,然后进行复杂地存取,特判是否相差2然后相加起来,最后再进行一次排序。如果你这样去做了,会发现很多的问题。首先是数组越界,相差2,相差1都需要判断,当i = n-1的时候你要判断,当n<3的时候,你还要判断与前一项的关系。总而言之,没有一个统一的式子能让你把他表示清楚。你在调试的时候花费大量的时间然后去检查代码的合理性,作为if-else仙人也难以搞定,这也是我在赛场上吸取的教训:思路相当重要,很多东西不是想当然的,而是推敲出来的。dp它之所以是一种强大的算法和大程度上得益于他的简洁性当然还有低的时间复杂度。
(这里不是要限制你的思维,好的方法你得承认嘛,之后我也会更新一题多解的文章,让大家看一下多种思路的碰撞并比较优劣)
言归正传,既然dp[i]代表的是到i处的最优解,那势必会取到前一次解和更新解的最大值,我们考虑用cnt[i]存取数字i的个数,思路出来了,考虑起点dp[0] = 0,终点i = 1e5,状态转移方程直接出:边界dp[1] = cnt[1](i = 1)以及dp[i] = max(dp[i-1],dp[i-2]+cnt[i]*i)(i>=2)
当然你要检验它的合理性。很合理的更新,cnt[i] = 0就用dp[i-1]覆盖,可以更到i = 1e5的上限就直接出结果。
上代码:
#include<iostream>
#define ll long long
using namespace std;
const int N = 1e5+5;
ll n,i,cnt[N],dp[N];
//记得long long全开,计算时可能爆int
int main()
{
cin >> n;
while(n--)
{
cin >>i;
cnt[i]++;//存入cnt[i],无所谓连不连号
}
dp[1] = cnt[1];
for(int i =2;i<=100000;i++)//干到上限就完事了
{
dp[i] = max(dp[i-1],dp[i-2] + cnt[i] * i);
}
cout<<dp[100000];
return 0;
}
一个注意点:long long全开,防止爆int。OI赛制还能按点拿分,acm赛制错就是全错了。
B:集邮[USACO3.1] 邮票 Stamps - 洛谷
(好吧,虽然比赛的时候写的是鸡油)
在我看来,输入格式第二段话挺扯的,就是他自己输入15个一行,不用管。其实题意很清楚,就是给定k张邮票,n种面值无数张的完全背包问题。稍微做改动的地方就是问你最大能凑成的连续数(第一个间断的不能凑成的数前一个数)值是多少。
属于完全背包的一个变式,大家先学习了背包问题的话会好理解些(我也是这周学的所以不熟练)
我们知道,完全背包是求解一定数目下,n种无穷多个物品ai的最大价值和,这里少掉一个参数:物品的体积(均为1),那就更好算点。现在要求的是哪个数是凑不出来的,看似没法下手,其实简单,只用标记好那个数就行。由此我们直接用dp[i]代表那个数i的状态,假如是初始值,那就是没动,没有办法凑成;假如更新过也就是!=初始值,就是可以凑成。最后遍历找到第一个断掉的i再减一。
起点是1,dp[1],终点是2e6,dp[i]代表至少要几张邮票,或者没法凑成。(即状态)
边界dp[0] = 0;//表示0由0张邮票凑成
状态转移方程:for(dp[i-a]+1<=k)//张数不能超k,筛掉不符合的
dp[i] = min(dp[i],dp[i-a]+1)//用更小的邮票数取得更优解
上代码:
#include<bits/stdc++.h>
using namespace std;
int k,n,i,j,s,a;//k:总共消耗的邮票数
const int N = 2e6; //i的上限
int dp[N];//dp[i]表示构成面值为i至少需要的邮票数
int main()
{
scanf("%d%d",&k,&n);
for(i = 1;i<=N;i++)dp[i] = 210;//标记,赋初值只要比k大就行
dp[0]=0;//0赋初值 ,用0张邮票可以构成0
for (i=1;i<=n;i++)
{
scanf("%d",&a);//读入
for (j=a;j<=N;j++)
if (dp[j-a]+1<=k)//用的邮票数目在范围内
dp[j]=min(dp[j],dp[j-a]+1);//取较小的
}
s=0;
for (i=1;i<=N;i++)//找从1开始 连续 的能取的集合的最后一个。
if (dp[i]==210)//凑不出了
{
s=i-1;//记录
break;//退出
}
printf("%d\n",s);//输出
return 0;
}
好了,本次讲解先到这里吧,其他的题目也补了一些,总体还是挺忙的,不过有收获就是好事。
思维的火花非常美妙,期待你我的下一次相逢。
(苏大第三次排位赛 vjudge密码:everyone_can_ak_dp)