深度优先搜索
深度优先搜索(Depth-First Search,简称DFS)是最常用的搜索方法之一,它的思想为“一条路走到底”。例如要遍历下图
若以0为起点,则dfs的遍历路径为0->1->4->5,此时已无路可走,于是便开始回退,直到1的位置,然后再沿另外一个方向开始遍历1->6->2。
其算法常为如下结构
void dfs(int step){
判断边界
尝试每一种可能 for(i=1;i<=n;i++)
{
继续下一步 dfs(step+1);
}
返回
}
P30.部分和问题
给定整数a1,a2,...an,判断是否可以从中选出若干数,使它们的和恰好为k。
限制范围:1<=n<=20 -10^8<=ai<=10^8 -10^8<=k<=10^8
分析
我做了很久一直没做出来。。。一方面是因为很久没看算法了,一方面则是掌握的还是不行。后来分析原因,在于递归的使用方式没有用好。怎么个没有用好呢?
首先,可以明确的是,要遍历所有可能,那么每个数都面临两种可能 ,被选中或者没有被选中,我的问题就出在这里,我想不到如何做到这点。
这一步的实现途径就是通过下面这两行代码
if(dfs(i+1,sum)) return true;
if(dfs(i+1,sum+a[i])) return true;
对两个情况进行两个判断,然后进行两个递归。
这是一种常见的方法,我也做过很多类似的,但是。。。可能是由于遗忘吧。
完整答案
#include<iostream>
using namespace std;
int a[20];
int n,k;
bool dfs(int i,int sum){
if(i == n) return sum == k;
if(dfs(i+1,sum)) return true;
if(dfs(i+1,sum+a[i])) return true;
return false;
}
int main(){
cout<<"n=";
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
cout<<"k=";
cin>>k;
if(dfs(0,0)) cout<<"yes"<<endl;
else cout<<"no"<<endl;
return 0;
}
P32.Lake Counting
有一个大小为N*M的园子,雨后有积水。八连通的积水被认为是连接在一起的。请求出园子里总共有多少水洼?
八连通指下图的*部分
* * *
*w*
* * *
限制条件:N,M<=100
分析
我的想法和书上的答案基本相同,都是先遍历图中的每一个位置,然后对符合条件的位置进行dfs。不同之处在于我还设了一个标记数组,用于标记已经走过的位置,而答案则是将走过的位置都由“w”替换为“.”,不得不说,还是答案更为精妙。
完整答案
#include<iostream>
using namespace std;
int n,m;
char a[20][20];
int sum=0;
void dfs(int i,int j);
int main(){
// 读入数据
cin<<n<<m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cin<<a[i][j];
}
}
// 遍历地图
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(a[i][j] == 'w'){
dfs(i,j);
sum++;
}
}
}
}
cout<<"sum="<<sum<<endl;
return 0;
}
void dfs(int i,int j){
a[i][j]='.';
for(int x=-1;x<2;x++){
for(int y=-1;y<2;y++){
int ti=i+x;
int tj=j+y;
if(b[ti][tj]==0 && ti>=0 && ti<n && tj>=0 && tj<m && a[ti][tj]=='w'){
dfs(ti,tj);
}
}
}
return;
}
宽度优先搜索
P34 迷宫的最短路径
给定一个大小为N*M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四格的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。
限制条件:N,M<=100
分析
做这道题时我也是挺费劲的,我定义了一个全局的总距离sum,但是在不同路径的下,如何将sum正确的累加,这个问题我不能解决。于是看了答案,答案是定义了一个数组,用于记录对应位置距离。知道了这个后我继续做,最终做出来的答案和书上的答案大体相同,不过我用的是递归,书上用的是while循环。
还有一点值得掌握的是书上用pair这个结构来记录位置,非常方便,关于pair的具体用法,可以看这里
完整答案
#include<iostream>
#include<queue>
#define T 999
using namespace std;
int n,m;
char t[100][100];
int b[100][100];
typedef pair<int,int> p;
queue<p> que;
int tx[4]={-1,1,0,0};
int ty[4]={0,0,-1,1};
//终点坐标和起点坐标
int gx,gy;
int sx,sy;
int bfs(int i,int j);
int main(){
// 读入数据
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cin>>t[i][j];
}
}
// 用于标记没有走过的位置
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
b[i][j]=T;
// 寻找入口和出口
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(t[i][j]=='S'){
sx = i; sy = j;
b[i][j]=0;
}
if(t[i][j]=='G'){
gx = i; gy = j;
}
}
}
cout<<bfs(sx,sy)<<endl;
return 0;
}
int bfs(int i,int j){
// 如果到达终点,返回最小距离
if(t[i][j] == 'G') return b[gx][gy];
// 向当前位置的上下左右依次尝试看能否前进
for(int x=0;x<4;x++){
int it=i+tx[x],jt=j+ty[x];
// 如果不是墙壁并且没有被标记过,则加入队列,并将距离加一
if(t[it][jt]!='#' && b[it][jt]==T && it>=0&&it<n&&jt>=0&&jt<m){
que.push(p(it,jt));
// 将距离加一
b[it][jt] = b[i][j]+1;
}
}
// 队列为空时还没有退出,说明没有找到终点。此时退出搜索,并返回0
if(!que.empty()){
p tmp = que.front(); que.pop();
bfs(tmp.first,tmp.second);
}else{
return 0;
}
}
贪心算法
P39 硬币问题
有1元,5元,10元,50元,100元,500元的硬币各C1,C5,C10,C50,C100,C500枚。现在要用这些硬币来支付A元,最少需要多少硬币?假定本题至少存在一种支付方案。
限制条件:0<=C1,C5,C10,C50,C100,C500<=10^9; 0<=A<=10^9.
分析
如果要使用的硬币最少,则需要尽可能地使用面额大的硬币。这个思路很容易得出,我按着这个思路也写出了答案,但是和书上的答案相比之下,有些繁琐了,不过更容易理解,书上的代码简洁,但是不是一眼就能看懂的。在这里把我的答案和书上的答案都附上。
我的代码
#include<iostream>
using namespace std;
// c[]记录各个值的初始硬币数,k[]记录对应种类硬币使用的个数
int c[6],k[6];
int v[6]={1,5,10,50,100,500};
int A;
int main(){
for(int i=0;i<6;i++)
cin>>c[i];
cin>>A;
int sum = 0;
// 从高价值的硬币的到低价值的硬币循环
for(int i=5;i>=0;i--){
while(sum+v[i]<=A){
if(c[i]>0){
sum += v[i];
k[i]++;
c[i]--;
}else{
break;
}
}
if(sum == A)
break;
}
int t=0;
for(int i=0;i<6;i++){
t+=k[i];
cout<<v[i]<<":"<<k[i]<<endl;
}
cout<<t;
return 0;
}
书上代码
// 硬币的面值
const int v[6] = {1,5,10,50,100,500}
int c[6];
int A;
void solve(){
int ans = 0;
for(int i=5;i>=0;i--){
int t = min(A/v[i],c[i]); //使用硬币i的枚数
A-=t*v[i];
ans+=t;
}
cout<<ans<<endl;
}
P40 区间问题
有n项工作,每项工作分别在Si时间开始,在ti时间结束。对于每项工作,你都可以选择参与与否。如果选择了参与,那么自始至终都必须全程参与。此外,参与工作的时间段不能重叠(即使是开始的瞬间和结束的瞬间的重叠也是不允许的)。你的目标是参与尽可能多的工作,那么最多能参与多少项工作呢?
限制条件:1<=N<=100000; 1<=Si<=ti<=10^9
分析
这道题我没有思路,虽然知道使用贪心算法,但是不知道该依照什么样的规则。看了解析后明白了其贪心的规则为“在可选的工作中,每次都选取结束时间最早的工作”。其证明如下:
- 与其他选择方案相比,该算法的选择方案在选取了相同数量的更早开始的工作时,其最终结束时间不会比其他方案的更晚。
- 所以,不存在选取更多工作的选择方案。
知道了规则后我自己又做了一下,刚开始觉得对,但是后来发现还是不全面。先把代码放出来吧。
#include<iostream>
#include<fstream>
using namespace std;
int main(){
// 工作的开始时间和结束时间
int s[5]={1,2,4,6,8};
int t[5]={3,5,7,9,10};
// 记录对于编号的作业是否能参与
int k[5]={0};
// 当前作业的编号
int p=0;
k[p]=1;
// 对当前每一份工作和之后的所有工作相比较,依次查找出结束时间最短的作业
for(int i=0;i<5;i++){
// 寻找在当前工作结束后开始的工作
if(s[i]>t[p]){
int low = t[i];
p=i;
for(int j=i;j<5;j++){
if(low>t[j]){
low=t[j];
p=j;
}
}
k[p]=1;
}
}
for(int i=0;i<5;i++)
cout<<k[i]<<" ";
return 0;
}
纰漏之处在于题目没有说明给的数据是排序过的,我直接把第一个作业看成了能参与的作业(K[p]=1),这样做等于默认它是按结束时间排序的。其他应该是没问题的,但是相对书上的答案复杂度高了些,而且代码更加的繁杂。
完整答案
const int MAX_N = 100000;
// 输入
int N,S[MAX_N],T[MAX_N];
// 用于对工作排序的pair数组
pair<int,int> itv[MAX_N];
void solve(){
// 对pair进行的是字典序比较
// 为了让结束时间早的工作排在前面,把T存入first,把s存入second
for(int i=0;i<N;i++){
itv[i].first = T[i];
itv[i].second = S[i];
}
sort(itv,itv+N);
// t是最后所选的结束时间
int ans = 0,t = 0;
for(int i=0;i<N;i++){
if(t<itv[i].second){
ans++;
t=itv[i].first;
}
}
cout<<ans<<endl;
}
P43 字典序最小问题:Best Cow Line
给定长度为N的字符串S,要构造一个长度为N的字符串T。起初,T是一个空串,随后反复进行下列任意操作。
- 从S的头部删除一个字符,加到T的尾部
- 从S的尾部删除一个字符,加到T的尾部
目标是要构造字典序尽可能小的字符串T。
限制条件:1<=N<=2000, 字符串S只包含大写英文字母
分析
刚开始想着只要不断通过比较S首尾的字母,把字典序小的加到T上就好了,但是后来发现事情并没有那么简单,因为当首尾为相同字母时,还要继续比较其次的字母,这个该怎么办?最后还是看的答案?。。。答案是用了两个用于标识的变量,我称之为“双指针”,因为像两个指针一样不断指着不同的位置,一个用于比较,一个用于输出。值得学习。
完整答案
#include<iostream>
using namespace std;
char s[20];
int n;
void solve(){
int a=0,b=n-1;
while(a<=b){
bool left = false;
// “双指针”,比较和输出是分开进行的,比较可以在范围内一直往下比较,但是输出是依次输出的
for(int i=0;a+i<=b;i++){
if(s[a+i]<s[b-i]){
left = true;
break;
}else if(s[a+i]>s[b-i]){
left = false;
break;
}
}
if(left) putchar(s[a++]);
else putchar(s[b--]);
}
putchar('\n');
}
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>s[i];
}
solve();
return 0;
}
P45 Saruman's Army
直线上有N个点。点i的位置是Xi。从这N个点中选择若干个,给它们加上标记。对每一个点,其距离为R以内的区域里必须有带有标记的点(自己本身带有标记的点,可以认为与其距离为0的地方有一个标记的点)。在满足这个条件的情况下,希望能为尽可能少的点添加标记。请问至少要有多少点被加上标记?
限制条件:1<=N<=1000, 0<=R<=1000, 0<=Xi<=1000
分析
找不到我写的的代码了,也可能是。。。我没做出来。就不分析了,直接附上书上的代码吧。
int N,R;
int X[MAX_N];
void solve(){
sort(X,X+N);
int i=0,ans=0;
while(i<N){
// s是没有被覆盖的最左的点的位置
int s=X[i++];
//一直向右前进直到距离大于R的点
while(i<N && X[i]<=S+R) i++;
//p是新加上标记的点的位置
int p=X[i-1];
//一直向右前进到距p的距离大于R的点
while(i<N && X[i]<=p+R) i++;
ans++;
}
cout<<ans<<endl;
}
P47 Fence Repair
农夫约翰为了修理栅栏,要将一块很长的木板切割成N块。准备切成的木板长度为L1,L2...LN,未切割前木板的长度恰好为切割后木板长度的总和。每次切断木板时,需要的开销为这块木板的长度。例如长度为21的木板要切成5,8,8的三块木板。长度21的木板切成长度为13和8的木板时,开销为21。再将长度为13的木板切成长度为5和8的木板时,开销是13.于是合计开销是34.请求出按照目标将木板切割完最小的开销是多少。
限制条件:1<=N<=20000, 0<=Li<=50000.
分析
最怕根本没有思路,而这题开始时我就是没有思路。现在是想通了,要想开销最小,那么就要先切割长度最长的分段,将长度最短的留在最后切割。因为最后切割的分段,其开销累积的次数也最多,所以自然要让它的长度最短。
以输入数据N=5,L={3,4,5,1,2}为例,整个切割过程对应了下列的二叉树
最后切割出的两段应该依次为当前最短的两段。这个二叉树清晰的体现了整个逻辑,当想到这个二叉树的时候,问题自然就解决了。开销的合计就是各叶子节点的 长度*深度。
我是直接根据这个二叉树结构写的代码,就是利用叶子节点数,求出其所在的深度,进而求出开销。
我的代码
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
int n;
int l[20];
// 二叉树的深度
int h;
// 二叉树最后一层和倒数第二层的叶子的个数
int a,b;
int sum;
void getnumber(int i);
void solve();
int main(){
cin>>n;
for(int i=0;i<n;i++)
cin>>l[i];
solve();
cout<<sum<<endl;
return 0;
}
// 给定叶子数,返回最后一层的叶子数和次层的叶子数
void getnumber(int x){
for(int i=1;i<20;i++){
// 深度为i的二叉树最后一层的叶子数为2^(i-1)
if(n<pow(2,i-1)){
h = i-1;
a = (x-pow(2,i-2))*2;
b = x-a;
return;
}
}
}
void solve(){
sort(l,l+n);
getnumber(n);
// 最后一层的叶子的开销
for(int i=0;i<a;i++){
sum+=l[i]*h;
}
// 倒数第二层的叶子的开销
for(int i=a;i<a+b;i++){
sum+=l[i]*(h-1);
}
}
而书上是依次令当前最短的两段相加,逻辑挺简单,但是代码写的我觉得有些复杂。
书上代码
typedef long long ll;
int N,L[MAX_N];
void solve(){
ll ans = 0;
// 每次都找到最短的两段
while(N>1){
int mii1 = 0,mii2 = 1;
if(L[mii1]>L[mii2]) swap(mii1,mii2); //使mii1小于mii2
// 使当前最小值保存在mii1中,次小值保存在mii2中
for(int i=2;i<N;i++){
if(L[i]<L[mii1]){
mii2 = mii1;
mii1 = i;
}else if(L[i]<L[mii2]){
mii2 = i;
}
}
// 每次将最短的两段相加
int t = L[mii1] + L[mii2];
ans += t;
if(mii1 == N-1) swap(mii1,mii2);
L[mii1] = t;
L[mii2] = L[N-1];
N--;
}
cout<<ans<<endl;
}
根据这个逻辑,我又写了一次,感觉跟容易理解了,更简单了。
我的代码2
int N,L[MAX_N];
void solve(){
// 先排序,然后依次令最短的两段相加
int ans=0;
sort(L,L+N);
int a=1,t=L[0]+L[1];
while(N>1){
ans+=t;
a++;
t += L[a];
N--;
}
cout<<ans<<endl;
}
动态规划
动态规划算是进阶的算法了吧。我前前后后学了起码该有三遍了,但是还是没有熟练掌握。一方面是难度大,一方面还是练的少。
动态规划的思想是“大事化小”,不断的把当前面临的问题转化成更小的子问题,但是使用的时候往往逆过来用,就是由问题的最小子状态(边界)不断推出更大的状态,最后解决整个问题。
这里有几个动态规划里的基本概念
- 最优子结构:状态可被分解为的子状态
- 边界:无需在继续简化的最小子状态
- 状态转移公式:状态与其子状态间关系的描述公式
解决动态规划的问题也往往从这几个概念入手。
P51 01背包问题
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值
限制条件:1<=n<=100, 1<=wi,vi<=100, 1<=W<=10000
分析
书上给了三个思路,这里我就用第二个吧,因为第二个是我最理解的。
首先找最优子结构,即可以把当前的状态分解成更小的子状态的关系式。我们设一个数组dp[MAX_N][MAX_W]用来存储第i个物品时重量限制为j的方案。这样讲可能理解不了,看下面这个图吧。
i为当前物品的序号,第0个物品是不存在的,但是后面要用到,所以也表示出来。j是当前物品的重量限制。例如第一个物品的重量为2,价值为3,那么填入表中就是这样
表中的数据是从第i个物品开始挑选,总重小于等于j时总价值的最大值。第二物品的重量为1,价值为2,继续填入表中,如下
在上表中第二个物品重量为1,所以dp[2][1]=2。dp[2][2]=3,因为在重量限制为2的情况下,如果要价值最大,则应该选择第一个物品。dp[2][3]=5,是因为在重量限制为3的情况下,如果要价值最大,可以同时选择第一个物品和第二个物品。
第三个物品重量为3,价值为4。第四个物品重量为2,价值为2。依次填入表中。
由上面表格可以得出该题的边界为 dp[0][j]=0
状态转移方程为
推导出这些就可以开始写代码了。
完整答案
#include<iostream>
#include<algorithm>
using namespace std;
int n,W;
int w[1000],v[1000];
int dp[100][100];
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>w[i]>>v[i];
}
cin>>W;
for(int i=0;i<=n;i++){
for(int j=1;j<=W;j++){
if(j<w[i]){
dp[i+1][j]=dp[i][j];
}else{
dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
}
}
}
cout<<dp[n][W];
return 0;
}
P56 最长公共子序列问题
给定两个字符串s1s2...sn和t1t2...tn。求出这两个字符串最长的公共子序列的长度。字符串s1s2...sn的子序列指可以表示为si1si2...sin(i1<i2<...<in)的序列。
限制条件:1<=n,m<=1000
分析
我没做出来,原因是虽然知道用动态规划,但是dp[i][j]里的i和j代表什么找不出来。不止如此,我还找不出动态转移方程,动态规划解题的最大难点莫过于动态转移方程了。看了书上的分析后算是豁然开朗,i,j分别代表这个初始字符串的中字母的位置,把其状态分解步骤列个表,就如下
接下来寻找状态转移方程。最长公共子序列简称LCS(Longest Commom Subsequence),假设dp[i][j]对应s1s2...si和t1t2...tj的LCS,则
当s(i+1)=t(j+1)时, s1s2...s(i+1)和t1t2...t(j+1)的LCS长度为dp[i][j]+1。
当s(i+1) != t(j+1)时,s1s2...s(i+1)和t1t2...t(j+1)的LCS长度为max(dp[i][j+1],dp[i+1][j])。
即可得到如下递推关系
完整答案
#include<iostream>
#include<cmath>
using namespace std;
int N,M;
char s[100],t[100];
int dp[100][100];
int main(){
cin>>N>>M;
for(int i=0;i<N;i++)
cin>>s[i];
for(int i=0;i<N;i++)
cin>>t[i];
for(int i=0;i<=N;i++){
for(int j=0;j<=M;j++){
if(s[i+1]==t[j+1]){
dp[i+1][j+1]=dp[i][j]+1;
}else{
dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
}
}
}
cout<<dp[N][M]<<endl;
return 0;
}
P57 完全背包问题
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以挑选任意多件。
限制条件:1<=n<=100, 1<=wi,vi<=100, 1<=W<=10000