算法复习


文章目录







二分

适用于有序查找,时间复杂度 O ( l o g n ) O(logn) O(logn)

核心算法

非递归版本

int BinarySearch1(int a[], int value, int n){
int low=0,high=n-1,mid;
while (low<=high){ // 终止条件 low>high
mid=(low+high)/2;
if(a[mid]==value) // 判断是否找到,可换为其他语句
return mid;
else if(a[mid]>value)
high=mid-1; // 区间中值在目标值右侧,故选择左半边,变更右边界
else if(a[mid]<value)
low=mid+1; // 区间中值在目标值左侧,故选择右半边,变更左边界
}
return -1; // 没找到
}

递归版本

int BinarySerch2(int a[], int value, int low, int high){
if(low>high) // 终止条件 low>high
return -1; // 没找到
int mid=low+(high-low)/2;
if(a[mid]==value) // 判断是否找到,可换为其他语句
return mid;
else if(a[mid]>value) // 区间中值在目标值右侧,故选择左半边,变更右边界
return BinarySerch2(a, value, low, mid-1);
else if(a[mid]<value) // 区间中值在目标值左侧,故选择右半边,变更左边界
return BinarySerch2(a, value, mid+1,high);
}

STL中的二分查找函数

lower_bound(start, end, value, compare)
upper_bound(start, end, value, compare)
binary_search(start, end, value,copmare)

​lower_bound​​的作用是在不减的数组中进行二分检索,找到大于等于​value​​​的第一个地址,不存在则返回地址​​end​

​upper_bound​​的作用是在不减的数组中进行二分检索,找到大于​value​​​的第一个地址,不存在则返回地址​​end​

​binary_search​​​的作用是在不减的数组中进行二分检索,判断​​value​​​在数组中是否存在,存在则返回​​ture​​​,不存在则返回​​false​

如在数组​​{1,2,2,2,3}​​​中,取​​value=2​​,则:


  • ​lower_bound​​返回的地址是第一个​​2​​的地址
  • ​upper_bound​​​返回的地址是**​​3​​的地址**

参数解释

​start​​ 参与查找的数组首地址,一般为​​数组名​

​end​​ 参与查找的数组尾地址+1,一般为​​数组名+数组长度​

​value​​ 目标值

​compare​​ 自定义的查找规则(比较函数),默认为上述规则。

对于递减数列,设置​​compare​​​参数为​​greater<type>()​​,可以实现查找“小于等于​​value​”和“小于​​value​”的功能,如在数组​​{3,2,2,2,1}​​​中,取​​value=2​​,则:


  • ​lower_bound(start, end, value, greater<int>())​​返回第一个​​2​​的地址
  • ​upper_bound(start, end, value, greater<int>())​​​返回**​​1​​的地址**

使用

由于函数返回的是地址,一般通过减去数组首地址的方式来获取目标的下标,如下:

int loc=lower_bound(a, a+len, value)-a; // 得到value对应的下标

例题 POJ-2456 Aggressive cows

中文版题目地址:​​天鹅棚​

在中国传媒大学的钢琴湖里有 C C C只天鹅,XH0822为它们建造了一座包括 N N N个隔间的天鹅棚,分别在坐标轴上的 x 1 , ⋯   , x N x_1,\cdots ,x_N x1,⋯,xN位置。但这些天鹅都彼此看不惯对方,为了防止他们互相伤害,所以不能把几只天鹅放在一个隔间里,而且还应该使两只天鹅之间的最小距离尽可能的大。可XH0822思考了好久,都不知道这个最大的最小距离是多少,你能不能帮帮他呢?

数据范围: 2 ≤ N ≤ 100000 , 0 ≤ x i ≤ 1 0 9 , 2 ≤ C ≤ N 2\leq N \leq 100000, 0 \leq x_i \leq 10^9, 2 \leq C \leq N 2≤N≤100000,0≤xi≤109,2≤C≤N

输入

有多组测试数据,以EOF结束

第一行包含两个整数 N N N和 C C C

后面接着有 N N N行,分别表示 x i x_i xi​的位置。(因为输入流可能很多,建议用​​scanf​​读取数据)

输出

每组测试数据输出一个整数,即题目中所说的最大的最小值。

输入样例

5 3
1
2
8
4
9

输出样例

3

参考思路及代码

二分枚举所有可能的距离 d d d


  • 距离的范围应为 0 ≤ d ≤ 隔间距离最大值 0 \leq d \leq \text{隔间距离最大值} 0≤d≤隔间距离最大值——明确区间的原始上下限
  • 对于每个 d d d,计算此时可以放鹅的房间数 n u m num num,并与题中要求的 C C C比较,如果​​num>=C​​,则说明当前的 d d d是满足条件的,我们去找有没有更大的 d d d也满足条件,即变更区间下限​​low=mid+1​​,反之则要缩小距离再次尝试,即变更区间上限​​high=mid-1​​——确定判断规则
  • 题目中给出的数组 x x x是无序的,需要先排序,以便后续判断

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
const int maxn=100020;
int x[maxn];
int C,N;
bool isEnough(int distence){
int num=1,separate_loc=0;
for(int i=1;i<N;i++)
if(x[i]>=x[separate_loc]+distence){
num++;
separate_loc=i;
}
if(num>=C)return 1;
return 0;
}
int BinarySearch(){
int low=0,high=x[N-1]-x[0],mid;
while (low<=high){
mid=(low+high)/2;
if(isEnough(mid))low=mid+1;
else high=mid-1;
}
//return low-1;
return high;

}
int main(){
cin>>N>>C;
for(int i=0;i<N;i++)
scanf("%d",&x[i]);
sort(x,x+N);
printf("%d",BinarySearch());
return 0;
}

简单数学

组合数

组合数的性质

【性质1】 C n m = n ! m ! × ( n − m ) ! C_n^m=\frac{n!}{m!\times (n-m)!} Cnm=m!×(n−m)!n!

【性质2】 C n k − 1 + C n k = C n + 1 k C_n^{k-1}+C_n^k=C_{n+1}^k Cnk−1+Cnk=Cn+1k

【性质3】 ∑ i = 0 n C n i = C n 0 + C n 1 + ⋯ + C n n = 2 n \sum\limits_{i=0}^nC_n^i=C_n^0+C_n^1+\cdots +C_n^n=2^n i=0∑nCni=Cn0+Cn1+⋯+Cnn=2n

组合数的计算

在 O ( n ) O(n) O(n)的时间内递推求出 C n 0 − C n n C_n^0-C_n^n Cn0−Cnn

递推关系:


  • C n 0 C_n^0 Cn0=1
  • C n k + 1 = C n k × n − k k + 1 C_n^{k+1}=C_n^k\times \frac{n-k}{k+1} Cnk+1=Cnk×k+1n−k

代码实现

long long C(int n, int m){
long long ans=1;
for(int i=0;i<m;i++)
ans=ans*(n-1)/(i+1);
return ans;
}
在 O ( n 2 ) O(n^2) O(n2)的时间内递推求出 C 0 0 − C n n C_0^0-C_n^n C00−Cnn

递推关系:


  • ∀ i ∈ [ 0 , n ] , C i 0 = 1 \forall i \in [0,n], C_i^0=1 ∀i∈[0,n],Ci0=1
  • C i j = C i − 1 j − 1 + C i − 1 j C_i^j=C_{i-1}^{j-1}+C_{i-1}^j Cij=Ci−1j−1+Ci−1j

实际上就是杨辉三角的纵向递推关系,每个值等于它上面的值和它左上方的值之和

n

C n 0 C_n^0 Cn0

C n 1 C_n^1 Cn1

C n 2 C_n^2 Cn2

C n 3 C_n^3 Cn3

C n 4 C_n^4 Cn4

0

1

1

1

1

2

1

2

1

3

1

3

3

1

4

1

6=3+3

4

1

代码实现

long long c[64][64];
void get(){ // 打表
for(int i=0;i<=60;i++)
c[i][0]=c[i][i]=1;
for(int i=1;i<=60;i++)
for(int j=1;j<=60;j++)
c[i][j]=c[i-1][j-1]+c[i-1][j];
return ;
}

组合数的应用|正整数拆分问题

已知正整数 N N N,可以将它拆分为若干正整数之和,共有 2 n − 1 2^{n-1} 2n−1种拆法

若拆成 k k k个正整数之和,则共有 C n − 1 k − 1 C_{n-1}^{k-1} Cn−1k−1种拆法(挡板法)

错排问题

已知有 n n n个人,每个人都有属于自己的一顶帽子。现在要重新分配这 n n n顶帽子,问有多少种不同的分配方法,使得这 n n n个人都没有拿到自己原来的那顶帽子

例如:


  • 当n=2时,有 ( a − > B , b − > A ) (a->B,b->A) (a−>B,b−>A)共1种分法。
  • 当n=3时,有 ( a − > B , b − > C , c − > A ) (a->B,b->C,c->A) (a−>B,b−>C,c−>A)和 ( a − > C , b − > A , c − > B ) (a->C,b->A,c->B) (a−>C,b−>A,c−>B)共2种分法。

递推公式: D n = ( n − 1 ) × ( D n − 1 + D n − 2 ) D_n=(n-1)\times (D_{n-1}+D_{n-2}) Dn=(n−1)×(Dn−1+Dn−2)

原理:

第 n n n个人拿着自己的帽子去和其他的 n − 1 n-1 n−1个人交换,他共有 ( n − 1 ) (n-1) (n−1)种选法。不妨假设第 n n n个人选中了第 k k k个人的帽子。接下来考虑第 k k k个人:


  • 如果他恰好拿的是自己的帽子,那么它们直接交换,剩下的 n − 2 n-2 n−2个人是错拿的,也就是 n − 2 n-2 n−2个人错排,只有1个人正确,即 D n − 2 D_{n-2} Dn−2。
  • 如果第 k k k个人拿的不是自己的帽子,那么它们也可以直接交换,那么意味着交换前这 n − 1 n-1 n−1个人都是错排的,即 D n − 1 D_{n-1} Dn−1 。


枚举排列

next_permutation(start, end, cmp)

​next_permutation​​​函数对从​​start​​​到​​end-1​​位置的元素进行按照字典序的下一个排列,如果排列成功则返回​​true​​​,否则返回​​false​​。

如对数组​​a[]={1,2,3};​​​运行一次​​next_permutation(a,a+3);​​​,再输出,得到的数组为​​{1,3,2}​

若改为​​next_permutation(a,a+2)​​​,则得到数组​​{2,1,3}​

可以通过​​do{}while(next_permutation());​​的方式获取全排列,前提是需要先将原数组升序排序,这样才能通过重复的“找下一个字典序排列”来获取所有排列

int a[]={1,2,3};
do {
for(int i=0;i<3;i++)
cout<<a[i]<<' ';
cout<<endl;
} while (next_permutation(a,a+3));

输出:

1 2 3 
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

对于第三个参数​​cmp​​​,可以参考​​sort​​中对用法,通过自定义比较规则来实现对结构体的应用。

例题 POJ1256 Anagram

可提交的题目地址:​​POJ256-vjudge.net​

描述

您将要编写一个程序,该程序必须根据给定的字母集生成所有可能的单词。

示例:给定单词“ abc”,您的程序应该-通过探索三个字母的所有不同组合-输出单词“ abc”,“ acb”,“ bac”,“ bca”,“ cab”和“ cba”。

在从输入文件中提取的单词中,某些字母可能会出现多次。对于给定的单词,您的程序不应多次产生相同的单词,并且单词应按字母升序输出。

输入

输入由几个词组成。第一行包含一个数字,该数字给出了要跟随的单词数。接下来的每一行包含一个单词。一个单词由从A到Z的大写或小写字母组成。大写和小写字母应被认为是不同的。每个单词的长度小于13。

输出

对于输入中的每个单词,输出应包含可以使用给定单词的字母生成的所有不同单词。由相同输入单词生成的单词应按字母升序输出。大写字母位于相应的小写字母之前。

输入样例

3
aAb
abc
acba

输出样例

Aab
Aba
aAb
abA
bAa
baA
abc
acb
bac
bca
cab
cba
aabc
aacb
abac
abca
acab
acba
baac
baca
bcaa
caab
caba
cbaa

提示

大写字母位于相应的小写字母之前。

因此字母的正确顺序是’A’<‘a’<‘B’<‘b’<… <‘Z’<‘z’。

参考题解

#include <iostream>
#include <algorithm>
using namespace std;
bool cmp(char a,char b){
if(tolower(a)!=tolower(b))
return tolower(a)<tolower(b); // 不同的字母,转换成小写后排序
else
return a<b; // 同一个字母,大写排在小写的前面
}
string s;
int main(){
int n;
cin>>n;
while (n--){
cin>>s;
sort(s.begin(),s.end(),cmp); // 先排序
do {
for(int i=0;i<s.size();i++)
cout<<s[i];
cout<<endl;
} while (next_permutation(s.begin(),s.end(),cmp));
}
return 0;
}

代码中提到了​​tolower​​函数,作用是对于一个字母:


  • 如果是大写字母,转换为小写字母
  • 如果是小写字母,不变

除此之外,​​toupper​​函数可以实现相反的功能,将字母全都转换为大写。

参考链接:​​next_permutation(a,a+n)​​ YogLn

prev_permutation(a,a+n,cmp)

与​​next_permutation​​​函数功能类似,​​prev_permutation​​函数用于获取按照字典序的上一个排列,如果获取成功则返回​​ture​​​,反之返回​​false​

如对序列​​a={3,2,1}​​​,使用一次​​prev_permutation(a,a+3)​​​后,得到序列​​{3,1,2}​

n皇后问题 HDU-2553

可提交的题目链接:​​棋盘摆放​

在一个大小为n*n的正方形棋盘上面摆放n个棋子,棋子间没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或同一列或同一对角线。

请你输出摆放n个棋子的所有可行的摆放方案C。

输入

输入含有多组测试数据。

每组数据共一行,为一个正整数 n n n ,表示棋盘大小为 n × n n\times n n×n,以及摆放棋子的数目为 n ( n ≤ 10 ) n(n \leq 10) n(n≤10)。当输入为 n = 0 n=0 n=0 时,表示输入结束。

输出

对于每组数据,输出一行,表示对应输入行的棋子摆放方案数 C C C。

输入样例

1
8
5
0

输出样例

1
92
10

思路

由题目要求,最后的摆法中每行有且仅有一个皇后每列也有且仅有一个皇后,因此最终的皇后坐标为 ( i , a i ) (i,a_i) (i,ai),其中 i = 1 , 2 , ⋯   , n i=1,2,\cdots, n i=1,2,⋯,n,而 { a n } \{a_n\} {an}也是 1 , 2 , ⋯   , n 1,2,\cdots ,n 1,2,⋯,n的一个排列。

此时,“不在同一行、列”的条件已被满足,只需要枚举所有可能的排列,再依次判断它们是否在同一对角线上,选出不在同一对角线上的排列即可。

参考题解

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int n,c=0;
int a[]={1,2,3,4,5,6,7,8,9,10};
int main(){

while (cin>>n&&n){
c=0;
do {
bool fl=1;
for(int i=0;i<n-1;i++)
if(fl)
for(int j=i+1;j<n;j++)
if(abs(i-j)==abs(a[i]-a[j])){
fl=0;break;
}
if (fl)c++;
} while (next_permutation(a,a+n));
cout<<c<<endl;
}
return 0;
}

上述代码是超时的,实际应用中可以根据n的范围打表提交。

#include <iostream>
using namespace std;
int main()
{
int n;
int a[15]={0,1,0,0,2,10,4,40,92,352,724};
while(cin>>n&&n)cout<<a[n]<<endl;
return 0;
}

枚举子集

用位向量法枚举子集,基本思路是用类似先序遍历二叉树的思想,枚举每一个位置的数是否被调用,算法实现如下:

// a-原集合
// used-当前a中每个位置的元素的状态(1表示选中,0表示未选中)
// cur表示现在在处理a[cur]
void printSubset(int a[], bool used[], int len, int cur){

if(cur==len){ // 如果目前已经枚举了最后一位,开始输出
for(int i=0;i<len;i++)
if(used[i])cout<<a[i]<<' ';
cout<<endl;
return ;
}
used[cur]= false;
printSubset(a, used, len, cur+1); // “遍历左儿子”
used[cur]= true;
printSubset(a, used, len, cur+1); // “遍历右儿子”
}

例题 Gym - 100712G Heavy Coins

可以提交的链接:​​花硬币​

描述

AltSuperBlade口袋里有很多硬币,这些硬币实在是太重了,所以AltSuperBlade希望尽快把这些硬币花掉。

AltSuperBlade刚好需要出门,他打算打车去目的地,这是一个很好的花掉硬币的机会,太开心啦!终于可以减轻负担了。

为了减少硬币数目,AltSuperBlade支付出租车司机费用的时候,他会尽量选择要支付的硬币数最多的情况。而出租车司机也有一个习惯,他不找零,但是也不会多收零钱,也就是只有当他不能取出一个或多个硬币使得AltSuperBlade支付的钱够打车费用时,他才会接受这些硬币。

例如,如果AltSuperBlade使用以下价值的硬币:2元、7元和5元来支付11元的打车费用,出租车司机便不会接受,因为价值2元的硬币被移除后剩下的钱仍够支付打车费用;但当AltSuperBlade只用7元和5元的硬币支付11元时,司机便会接受,因为如果减掉5元或者7元,剩下的钱就不够支付打车费用了。

输入

输入的第一行包含 t ( 1 ≤ t ≤ 1001 ) t(1≤t≤1001) t(1≤t≤1001),测试用例的数量。

每个测试用例的第一行包含两个整数: n ( 1 ≤ n ≤ 10 ) n(1≤n≤10) n(1≤n≤10)和 s ( 1 ≤ s ≤ 1000 ) s(1≤s≤1000) s(1≤s≤1000),其中 n n n是AltSuperBlade口袋中的硬币数量, s s s是AltSuperBlade必须为出租车司机支付的金额(单位:元)。下一行包含 n n n个空格分隔的整数,介于1到100之间,表示AltSuperBlade口袋中硬币的面值(以元为单位)。

输出

对于每组测试用例,打印一行AltSuperBlade可以用来支付打车费用的最大硬币数。

输入样例

2
5 9
4 1 3 5 4
7 37
7 5 8 8 5 10 4

输出样例

3
6

提示

在第一个测试用例中,AltSuperBlade可以用以下任何方式支付: ( 1 , 3 , 5 ) (1,3,5) (1,3,5), ( 3 , 4 , 4 ) (3,4,4) (3,4,4)或 ( 1 , 4 , 4 ) (1,4,4) (1,4,4)

参考题解

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int t,n,s,ans;
bool used[15];
int coins[15];
void findSubset(int a[], bool used[], int len, int cur, int &ans){
if(cur==len){
int sum=0, mi=200,num=0;
for(int i=0;i<len;i++){
if(used[i]){
sum+=a[i]; // 计算当前子集的钱数
num++; // 计算当前子集的硬币数
mi=min(mi,a[i]); // 记录当前硬币子集的最小面值
}
}
if(sum>=s&&sum-mi<s)
ans=max(ans,num); // 满足条件则更新ans的值
return;
}
used[cur]=0;
findSubset(a, used, len, cur+1, ans);
used[cur]=1;
findSubset(a, used, len, cur+1, ans);
}
int main(){
cin>>t;
while (t--){
memset(used,0,sizeof(used)); // 刷新used数组
memset(coins,0,sizeof(coins)); // 刷新coins数组
ans=0; // 刷新ans的值
scanf("%d%d",&n,&s);
for(int i=0;i<n;i++)
scanf("%d",&coins[i]);
findSubset(coins,used,n,0,ans);
printf("%d\n",ans);
}
return 0;
}

贪心

在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。但是由于题目某种性质导致每次取局部最优,便会得到全局最优。

贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。

算法思路


  1. 建立数学模型来描述问题
  2. 把求解的问题分成若干个子问题
  3. 对每一子问题求解,得到子问题的局部最优解
  4. 把子问题的局部最优解合成原来问题的解

样例 国王游戏

描述

恰逢 H 国国庆,国王邀请 n n n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 $n $位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。

国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序, 使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。

解题思路

设 a i , b i a_i,b_i ai,bi分别为第 i i i 位大臣左右和右手的数字,按 a i × b i a_i\times b_i ai×bi 由小到大排序

样例 活动选择问题

描述

设有 n ( n < = 1 0 6 ) n(n<=10^6) n(n<=106)个活动的集合$E ={1,2,…,n } $,其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动 i i i 都有一个要求使用该资源的起始时间 s i s_i si 和一个结束时间 f i f_i fi ,且 s i < f i s_i < f_i si<fi 。现在给出 n n n 个活动使用资源的起始时间 s i s_i si 和结束时间 f i f_i fi ,请你帮助办公室人员安排活动,要求排的活动尽量多 。

解题思路

按结束时间由小到大排序,优先选取结束时间早的节目。


深度优先搜索DFS

算法思想

从初始状态开始,一直往深处走,直到找到解或走不下去为止

递归框架

dfs(dep, ...){ // dep代表目前dfs的深度
if(找到解||走不下去){
...
return ;
}
dfs(dep+1, ...);// 枚举下一种情况
}

CodeForces-1097B Petr and a Combination Lock

中文可提交链接:​​组合锁​

描述

2019302120031YU 刚刚买了一辆新车,他把车开到加油站准备加油,突然发现油箱上有一把密码锁!但他不知道怎么解锁。锁的刻度为360度,指针一开始指向0。


于是2019302120031YU 给他的汽车经销商打了个电话,他的经销商让他把锁准确地转动n次。第 i 次旋转应该是 ai 度,顺时针或逆时针转都可以,n次旋转之后指针应该再次指向0,即可解锁。

这让2019302120031YU 有点困惑,他不确定哪次旋转应该顺时针,哪次应该逆时针,因为旋转锁的方法有很多。已知输入的数据是每1次锁应该旋转的度数,请你帮助他判断一下是否至少存在一种旋转方法,在n次旋转之后指针会再次指向0。

输入

第 1 行输入 1 个整数 n (1 <= n <= 15)—— 代表旋转次数。

接下来的 n 行里,每 1 行包含 1 个整数 ai (1<=ai<=180)—— 代表旋转度数。

输出

如果做完所有的旋转后,指针能指向0,那么输出一个单词“YES”。否则,输出“NO”。在这种情况下,2019302120031YU 会要求经销商给它换一辆新车。

样例

输入样例1

3
10
20
30

输出样例1

YES

输入样例2

3
10
10
10

输出样例2

NO

输入样例3

3
120
120
120

输出样例3

YES

参考题解

#include <iostream>
using namespace std;
int n;
int a[30];
bool isExist=0; //
void dfs(int dep, int sum){
if(dep==n){
if(sum%360==0)isExist=1;
return;
}
dep++;
dfs(dep,sum+a[dep]);
dfs(dep,sum-a[dep]);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];

dfs(0,0);
if(isExist)cout<<"YES";
else cout<<"NO";
return 0;
}

POJ-3984 迷宫问题 dfs版

可提交的中文版题目:​​迷宫问题​

描述

定义一个二维数组:

int maze[5][5] = {

0, 1, 0, 0, 0,

0, 1, 0, 1, 0,

0, 0, 0, 0, 0,

0, 1, 1, 1, 0,

0, 0, 0, 1, 0,

};

它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。

Input

一个5 × 5的二维数组,表示一个迷宫。数据保证有唯一解。

Output

左上角到右下角的最短路径,格式如样例所示。

Sample Input

0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0

Sample Output

(0, 0)
(1, 0)
(2, 0)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 4)
(4, 4)

参考题解

#include <iostream>
#include <stack>
using namespace std;
int map[7][7];
bool isVis[7][7]; // 用于标记是否被访问过
const int dx[]={0, 1, 0, -1};
const int dy[]={1, 0, -1, 0}; // 方向数组,顺序是下右上左(逆时针)
struct point{
int x, y;
};
point visLine[30]; // 用于存储当前的最段路径
stack<point> s;
int minLen=30; // 记录最短路径的长度

void dfs(){
point p=s.top(); // 取栈顶元素

if(p.x==5&&p.y==5){ // 找到终点
if(s.size()<minLen){ // 更新最小值和路径
minLen=s.size();
for(int i=minLen-1;i>=0;i--)
visLine[i]=s.top(),s.pop(); // 存储最短路径
for(int i=0;i<minLen;i++)
s.push(visLine[i]); // 存好后恢复栈
}
s.pop();
return; // 结束
}

point next;
while (!s.empty()){
bool canVis= false; //判断是否有路可走
for(int i=0;i<4;i++){ // 方向数组派上用场
next.x=p.x+dx[i];
next.y=p.y+dy[i];
if(!map[next.x][next.y]&&!isVis[next.x][next.y]){ // 不是障碍物&&没被访问过
s.push(next); // 入栈
isVis[next.x][next.y]= true; // 标记该点已访问
dfs();
canVis= true; // 有路可走
}
}
if(!canVis) // 无路可走则出栈
s.pop();
}
}
int main(){
for(int i=1;i<=5;i++)
for(int j=1;j<=5;j++)
cin>>map[i][j];
for(int i=0;i<=6;i++)
map[0][i]=map[6][i]=map[i][0]=map[i][6]=1; // 给四周加墙以防越界
point begin={1,1};
s.push(begin);
isVis[1][1]= true;
dfs();
for(int i=0;i<minLen;i++)
cout<<'('<<visLine[i].x-1<<", "<<visLine[i].y-1<<')'<<endl; //由于加墙了,坐标需要减一

return 0;
}

广度优先搜索BFS

算法思想


  1. 从初始状态S开始,利用一定的规则,生成所有下一层的状态,依次入队
  2. 依次出队,顺序检查所有状态,看是否出现目标状态G,如果没出现,就对它们分别利用规则,生成再下一层的所有状态
  3. 找到目标G

算法框架

queue<type> q;
q.push({初始状态}); // 初始状态入队
isVis[初始状态]=1; // 标记初始状态为已访问
while(!q.empty()){
type p=q.front(); // 取队首
q.pop(); // 当前队首直接出队
if(p==目标状态){
...;
break;
}
所有与p相邻且未访问的结点入队,并标记它们为已访问;
}

在搜索过程中,bfs对于节点是沿着深度一层一层扩展的,也就是说要扩展到第 n + 1 n+1 n+1层时,第n层已经被访问完了。

同一层对于问题的求解有着相同的价值。

适用于找最短路径

HDU-1253 胜利大逃亡

可提交中文题目:​​胜利大逃亡​

描述

Ignatius被魔王抓走了,有一天魔王出差去了,这可是Ignatius逃亡的好机会.

魔王住在一个城堡里,城堡是一个 A × B × C A\times B \times C A×B×C的立方体,可以被表示成A个 B × C B\times C B×C的矩阵,刚开始Ignatius被关在 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)的位置,离开城堡的门在 ( A − 1 , B − 1 , C − 1 ) (A-1,B-1,C-1) (A−1,B−1,C−1)的位置,现在知道魔王将在T分钟后回到城堡,Ignatius每分钟能从一个坐标走到相邻的六个坐标中的其中一个.现在给你城堡的地图,请你计算出Ignatius能否在魔王回来前离开城堡(只要走到出口就算离开城堡,如果走到出口的时候魔王刚好回来也算逃亡成功),如果可以请输出需要多少分钟才能离开,如果不能则输出-1.

算法复习|汇总_动态规划

Input

输入数据的第一行是一个正整数K,表明测试数据的数量.每组测试数据的第一行是四个正整数A,B,C和T(1<=A,B,C<=50,1<=T<=1000),它们分别代表城堡的大小和魔王回来的时间.然后是A块输入数据(先是第0块,然后是第1块,第2块…),每块输入数据有B行,每行有C个正整数,代表迷宫的布局,其中0代表路,1代表墙.(如果对输入描述不清楚,可以参考Sample Input中的迷宫描述,它表示的就是上图中的迷宫)

特别注意:本题的测试数据非常大,请使用scanf输入,我不能保证使用cin能不超时.在本OJ上请使用Visual C++提交.

Output

对于每组测试数据,如果Ignatius能够在魔王回来前离开城堡,那么请输出他最少需要多少分钟,否则输出-1.

Sample Input

1
3 3 4 20
0 1 1 1
0 0 1 1
0 1 1 1
1 1 1 1
1 0 0 1
0 1 1 1
0 0 0 0
0 1 1 0
0 1 1 0

Sample Output

11

参考题解

#include <iostream>
#include <queue>
#include <cstdio>
#include <cstring>
using namespace std;
int k,a,b,c,t,st; // st用于记录当前步长

const int dx[]={0, 0, 1, 0, 0, -1}; // 三维方向数组
const int dy[]={0, 1, 0, 0, -1, 0};
const int dz[]={1, 0, 0, -1, 0, 0};

struct point{
int x,y,z;
int step;
};
queue<point> q;
bool isVis[55][55][55];
void clearQ(){
while (!q.empty())
q.pop();
}
int map[55][55][55];
bool isFind=0;

bool isAvailale(point p){
if(p.x<0||p.y<0||p.z<0)return false;
if(p.x>a-1||p.y>b-1||p.z>c-1)return false;
if (isVis[p.x][p.y][p.z])return false;
if(map[p.x][p.y][p.z])return false;
return true;
}

void bfs(){
q.push({0,0,0,0});
isVis[0][0][0]=1;
while (!q.empty()){
point cur=q.front();
if (cur.step>t)return; // 剪枝
q.pop();
if(cur.x==a-1&&cur.y==b-1&&cur.z==c-1){
isFind=1;
st=cur.step;
return;
}
for(int i=0;i<6;i++) {
point next;
next.x = cur.x + dx[i];
next.y = cur.y + dy[i];
next.z = cur.z + dz[i];
next.step = cur.step + 1;
if (isAvailale(next)){
isVis[next.x][next.y][next.z]= true;
q.push(next);
}
}
}
}

int main(){
scanf("%d",&k);
while (k--){
clearQ();
memset(isVis,0,sizeof(isVis));
memset(map,0, sizeof(map));
isFind=0;
scanf("%d%d%d%d",&a,&b,&c,&t);
for(int i=0;i<a;i++)
for(int j=0;j<b;j++)
for(int p=0;p<c;p++)
scanf("%d",&map[i][j][p]);
bfs();
if(isFind&&st<=t)printf("%d\n",st);
else printf("-1\n");
}
return 0;
}

HDU-1241 Oil Deposits

中文版可提交题目:​​石油探测​

GeoSurvComp地质调查公司负责探测地下石油储藏。 GeoSurvComp现在在一块矩形区域探测石油,并把这个大区域分成了很多小块。他们通过专业设备,来分析每个小块中是否蕴藏石油。如果这些蕴藏石油的小方格相邻,那么他们被认为是同一油藏的一部分。在这块矩形区域,可能有很多油藏。你的任务是确定有多少不同的油藏。

Input

输入可能有多个矩形区域(即可能有多组测试)。每个矩形区域的起始行包含m和n,表示行和列的数量,1<=n,m<=100,如果m =0表示输入的结束,接下来是m行,每行n个字符。每个字符对应一个小方格,并且要么是’*’,代表没有油,要么是’@’,表示有油。

Output

对于每一个矩形区域,输出油藏的数量。两个小方格是相邻的,当且仅当他们水平或者垂直或者对角线相邻(即8个方向)。

Sample Input

1 1
*
3 5
*@*@*
**@**
*@*@*
1 8
@@****@*
5 5
****@
*@@*@
*@**@
@@@*@
@@**@
0 0

Sample Output

0
1
2
2

参考题解

#include <iostream>
#include <queue>
#include <cstring>
#include <cstdio>
using namespace std;
int T;
int n,m,num; // m-line
char map[130][130];
bool isVis[130][130];
struct point{
int x,y;
};
int dx[]={0,1,1,1,0,-1,-1,-1};
int dy[]={1,1,0,-1,-1,-1,0,1};
queue<point> q;
void clearQ(){
while (!q.empty())
q.pop();
}

int main(){

while (cin>>m>>n&&m&&n){
memset(isVis,0,sizeof(isVis));
num=0;
for(int i=0;i<m;i++)
cin>>map[i];
for(int i=0;i<m;i++)
for(int j=0;j<n;j++){
if(map[i][j]=='@'&&!isVis[i][j]){
num++;
isVis[i][j]= true;
q.push({i,j});
while (!q.empty()){
point cur=q.front();
q.pop();
for(int k=0;k<8;k++){
int nx=cur.x+dx[k];
int ny=cur.y+dy[k];
if(nx<0||ny<0||nx>m-1||ny>n-1)continue;
if(isVis[nx][ny])continue;
if(map[nx][ny]=='@')
q.push({nx,ny}),isVis[nx][ny]=1;
}
}
}
}
cout<<num<<endl;
}
return 0;
}

基础动态规划

基本概念

动态规划​​dynamic programming​​通常基于一个递推公式——状态转移方程及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出

能用动态规划解决的问题的特点


  1. 问题具有最优子结构性质。如果如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
  2. 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态没有关系。


**【无后效性】**现在有一个n*m的网格,网格每一个点上分别有一个价值,左上角有一个棋子,棋子每次只能往下走或者往右走,现在要让棋子走到右下角,求最大价值。

f [ i ] [ j ] f[i][j] f[i][j]表示走到第i行第j列的位置获得的最大价值,当位于 ( i , j ) (i,j) (i,j)的棋子要进行决策(向右或者向下走)的时候,之前棋子是如何走到 ( i , j ) (i,j) (i,j)这个位置的是不会影响做这个决策的。之前的决策不会影响未来的决策,这就是无后效性,也就是所谓的“未来与过去无关”

**【有后效性】**还是以上面的走棋子为例,只不过现在规则变为:棋子可以上下左右走但是不能走重复的格子。

若还以 f [ i ] [ j ] f[i][j] f[i][j]表示走到 ( i , j ) (i,j) (i,j)获得的最大价值,是存在后效性的,当前的决策是受到之前的决策的影响的,因为之前走过的点之后不能再走了,而这种状态表示方式并不能知道之前走过了哪些点。


记忆化搜索

每算出来一个 M a x S u m [ r , j ] MaxSum[r,j] MaxSum[r,j]就保存起来,下次用到的时候直接取用,以避免重复计算

POJ-1163

题目链接:​​The Triangle​

7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

(Figure 1)

给定一个由n行数字组成的数字三角形如上图所示。试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。对于给定的由n行数字组成的数字三角形,计算从三角形的顶至底的路径经过的数字和的最大值。

Input

输入数据的第1行是数字三角形的行数n,1≤n≤100。接下来n行是数字三角形各行中的数字。所有数字在0…99之间。

Output

输出数据只有一个整数,表示计算出的最大值。

Sample Input

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

Sample Output

30

状态

M a x S u m [ r , j ] MaxSum[r,j] MaxSum[r,j]

状态转移方程

M a x S u m [ r , j ] = D [ r , j ] + m a x { M a x S u m [ r + 1 , j ] , M a x S u m [ r + 1 , j + 1 ] } MaxSum[r,j]=D[r,j]+max\{MaxSum[r+1,j],MaxSum[r+1,j+1] \} MaxSum[r,j]=D[r,j]+max{MaxSum[r+1,j],MaxSum[r+1,j+1]}

参考题解

#include <iostream>
using namespace std;
int dp[120][120];
int n;
int tri[120][120];
int main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>tri[i][j];
for(int i=n;i>=1;i--)
for(int j=1;j<=i;j++)
dp[i][j]=tri[i][j]+max(dp[i+1][j],dp[i+1][j+1]); // 状态转移
cout<<dp[1][1];
return 0;
}

背包问题

01背包

给定一个最大载重量为 m m m 的卡车和 n n n 种物品。已知第 i i i 种物品的重量为 w i w_i wi 公斤,其商品价格为 v i v_i vi 元,编程确定一个装货方案,使得装入卡车中的所有物品总价值最大。

状态

d p [ i ] [ j ] dp[i][j] dp[i][j]

第一维表示物品编号,第二维度表示剩余空间

对于第 i i i个物品,可以拿它也可以不拿它


  • 如果拿它,总价值变为 d p [ i − 1 ] [ j − w i ] + v i dp[i-1][j-w_i]+v_i dp[i−1][j−wi]+vi,即等于之前的背包(给它留出空间)的总价值和它的价值之和
  • 如果不拿,总价值还是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]

因此,当前总价值的最大值为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w i ] + v i ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w_i]+v_i) dp[i][j]=max(dp[i−1][j],dp[i−1][j−wi]+vi)

算法实现

一维版本

for(int i=0;i<n;i++)
for(int j=m;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[m]; // 答案存在dp[总空间]中

HDU-2602 Bone Collector

题目链接:​​Bone Collector​

描述

The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?

Input

The first line contain a integer T , the number of cases.

Followed by T cases , each case three lines , the first line contain two integer N , V, (N <= 1000 , V <= 1000 )representing the number of bones and the volume of his bag. And the second line contain N integers representing the value of each bone. The third line contain N integers representing the volume of each bone.

Output

One integer per line representing the maximum of the total value (this number will be less than 231).

Sample Input

1
5 10
1 2 3 4 5
5 4 3 2 1

Sample Output

14

参考题解

#include <iostream>
#include <cstring>
using namespace std;
int dp[1200];
int n,m; // 总空间为m
int v[1200],w[1200];

int main(){
int T;
cin>>T;
while (T--){
cin>>n>>m;
memset(dp,0, sizeof(dp));
for(int i=0;i<n;i++)
cin>>v[i];
for(int i=0;i<n;i++)
cin>>w[i];
for(int i=0;i<n;i++)
for(int j=m;j>=w[i];j--)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
cout<<dp[m]<<endl;
}
return 0;
}

其他背包的基本代码实现

完全背包

东西可以无限拿

for(int i =0;i<n;i++)
for(int j=w[i];j<=m;j++) // 颠倒上下限即可
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
多重背包

给出一个数组 c o u n t count count用于表示每种物品能拿多少件

for(int i=1;i<=n;i++){
int k;
for(k=1;k*2<count[i]+1;k*=2)
for(int j=V;j>=k*c[i];j--)
dp[j] = max(dp[j],dp[j-k*c[i]] + k*a[i]);
k = count[i] + 1 - k;
for(int j = V; j >= k*c[i]; --j)
dp[j] = max(dp[j],dp[j-k*c[i]] + k*a[i]);

}

最长公共子序列LCS问题

对于两个字符串 A , B A,B A,B ,用 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 A A A 中前 i i i 个字符和 B B B 中前 j j j 个字符的最大公共子串,则有:

d p [ i ] [ j ] = { 0 i = j = 0 d p [ i − 1 ] [ j − 1 ] + 1 A i = B j m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) A i ≠ B j dp[i][j]= \begin{cases} 0 & i=j=0 \\ dp[i-1][j-1]+1 & A_i=B_j \\ max(dp[i-1][j],dp[i][j-1]) &A_i \not = B_j \end{cases} dp[i][j]=⎩⎪⎨⎪⎧​0dp[i−1][j−1]+1max(dp[i−1][j],dp[i][j−1])​i=j=0Ai​=Bj​Ai​​=Bj​​

HDU-1159

题目链接:​​HDU-1159​

描述

tom与jerry在玩一个游戏。

他们先各自写下一串字符,然后互相展示。展示过后,他们再从自己写的那串字符中依次挑出若干字符(保持原有顺序不变),组成新的一串。他们希望自己新组成的字符串与对方新组成的完全相同,并且尽可能长。

例如,tom写下abcde,jerry写下aeiou,然后tom挑出自己那串里的第1和第5个字符组成新串ae,jerry挑出自己那串中的第1、2个字符,也组成字符串ae。ae就是他们能共同挑出的最长串。

现在,tom和jerry分别写出了自己的字符串,请帮他们算一下他们能共同挑出组成的字符串最长有多长。

Input

输入包含多组数据,处理至文件结尾。

每组数据占一行,包括以空格分隔的两个字符串,分别是tom和jerry写下的字符串。两个字符串长度都在1000以内。

Output

对于每组输入,输出一个整数,即他们能共同挑出组成的字符串的最大长度。

Sample Input

abcfbc abfcab
programming contest
abcd mnp

Sample Output

4
2
0

参考代码

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn=1200;
int dp[maxn][maxn];
char a[maxn],b[maxn];

int main(){
while (~scanf("%s %s",a,b)){
memset(dp,0, sizeof(dp));
int lena=strlen(a),lenb=strlen(b);
// i,j 表示子串中的字符个数,取值范围为[0,lena(lenb)],实际下标需要-1
for(int i=1;i<=lena;i++)
for(int j=1;j<=lenb;j++){
if(a[i-1]==b[j-1]) // 当前字符相同则公共子串长度+1
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
cout<<dp[lena][lenb]<<endl;
}
return 0;
}