包括模拟、暴力、枚举、贪心、递推与递归、二分、搜索。
值得注意的是,一道题的解法往往不止一种。
目录
P1219 [USACO1.5] 八皇后 Checker Challenge
一、搜索
P1219 [USACO1.5] 八皇后 Checker Challenge
P1219 [USACO1.5] 八皇后 Checker Challenge https://www.luogu.com.cn/problem/P1219
经典模板题n皇后
题目数据为n皇后的部分情况(即6≤n≤13)
题目重点:nxn(棋盘的宽高一样,限值了变量的表达(如行号列号和对角线))
//下列代码适用于n的多种取值
//输出函数,用于在大于3个解时输出前3个解,小于等于3个解时输出所有解
/*
输出格式:前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
*/
inline int print(){
ans++;
if(ans<=3)//小于3个解则直接罗列,否则罗列前3个解并输出解总数
for(int i=1;i<=n;i++){
if(i==n)//各解的情形输出后换行
write(f[i]),printf("\n");
else printf("%d ",f[i]);
}
}
//用dfs方法求解 ,参数j用于枚举,表示当前的行数。运行时从1开始,向下寻找解题答案
inline void pd(int j){
if(j>n){
print();
return;
}
else {
for(int a=1;a<=n;a++){//a表示当前方案中棋子的标号(n是多少棋盘就放几枚棋子)
if(!col[a]&&!k[j-a+n]&&!h[a+j]){
//从第一行/列开始枚举 。如果这一列的这个位置没有棋子占领,所在的两个对角线也没有棋子
//其中两个对角线对应的下标可证是j-a+n和a+j
f[j]=a,//记录方案
col[a]=1,//标记放置
h[a+j]=1,k[j-a+n]=1;
pd(j+1);//继续搜索下一枚棋子。
//注意,调用下一行的情况是下一个棋子的摆放方案不存在,那么清除之前的赋值,回到上一步继续搜索
col[a]=0,k[j-a+n]=0,h[a+j]=0;
}
}
}
}
//pd中间有一个调用自身的过程,实现了在一个方案中一行行往下找的过程
int main(){
read(n);
pd(1);
write(ans);
return 0;
}
P2670 [NOIP2015 普及组] 扫雷游戏
https://www.luogu.com.cn/problem/P2670http://P2670 [NOIP2015 普及组] 扫雷游戏
搜索+模拟,不难。
int n,m;
char c[105][105],ans[105][105];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>c[i][j];
ans[i][j]=48;
//ASCII码中48对应‘0’,49对应‘1’,以此类推。
//我们用字符数组记录每个格子周围的地雷数量。如果这个格子是地雷,直接用*标记为地雷。
//在输入初始雷区的同时初始化按下标记录地雷的数组
}
}
for(int i=1;i<=n;i++) {//开始对雷区逐个排查
for(int j=1;j<=m;j++) {
if(c[i][j]=='*')//这个格子是地雷吗?是就标记
ans[i][j]='*';
if(c[i][j]=='?'){//这个格子不是地雷,就排查它四面八方的地雷数量
if(c[i-1][j-1]=='*')
ans[i][j]++;
if(c[i-1][j]=='*')
ans[i][j]++;
if(c[i-1][j+1]=='*')
ans[i][j]++;
if(c[i][j-1]=='*')
ans[i][j]++;
if(c[i][j+1]=='*')
ans[i][j]++;
if(c[i+1][j-1]=='*')
ans[i][j]++;
if(c[i+1][j]=='*')
ans[i][j]++;
if(c[i+1][j+1]=='*')
ans[i][j]++;
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)
cout<<ans[i][j];
cout<<endl;
}
return 0;
}
二、贪心
P1478 陶陶摘苹果(升级版)
P1478 陶陶摘苹果(升级版) https://www.luogu.com.cn/problem/P1478
题意:只有摘苹果才消耗力气。所以,在高度允许的范围内,消耗力气摘尽可能多的苹果。
int a,b,n,s,num;
/*
每个苹果都有的属性:到达地上的高度和陶陶摘它需要的力气
因此,我们给苹果写一个结构体
*/
struct taotao{
int x;
int y;
} apple[MAXN];
bool cmp(taotao p,taotao q){//手写对结构体的排序规则 ,不可忽略
if(p.y<q.y)//先按苹果要求的力气排序,花费力气少的放前面
return 1;
else return 0;
if(p.y==q.y){//花费力气相同的情况下,高度低的放前面,方便摘苹果时按高度比较
if(p.x<=q.x)
return 1;
else return 0;
}
}
int main(){
cin>>n>>s;
cin>>a>>b;
for(int i=1;i<=n;i++)
cin>>apple[i].x>>apple[i].y;
sort(apple+1,apple+1+n,cmp);//排序
for(int i=1;i<=n;i++){
//if内的两个条件:能够到苹果,力气也够用
if((a+b)>=apple[i].x&&s>=apple[i].y){
s-=apple[i].y;//摘到苹果后消耗的力气要减去
num++;//摘到的苹果数量+1
}
}
cout<<num<<endl;
return 0;
}
P1094 [NOIP2007 普及组] 纪念品分组
P1094 [NOIP2007 普及组] 纪念品分组 https://www.luogu.com.cn/problem/P1094
贪心+排序
每组为1-2个纪念品
int j=1,pri[30006],w,n,ans;
int main(){
cin>>w>>n;
for(int i=1;i<=n;i++)
cin>>pri[i];
sort(pri+1,pri+1+n);//先对纪念品升序排序方便分组
//升序排序后开头结尾向
while(n>=j){//此时,n表示未分组的纪念品数组末尾元素下标,j是未分组纪念品开头元素下标
if(pri[n]+pri[j]<=w){//开头结尾,注意分组的前提是价格之和不超过上限
n--;
j++;
ans++;
}
else {//超过上限的话就把靠后的元素单独一组,从后缩小未分组纪念品的数量
n--;
ans++;
}
}
cout<<ans<<endl;
return 0;
}
P2678 [NOIP2015 提高组] 跳石头
重点:贪心、二分
题目对算法的限制主要在于“最大值最小”,拿走石头的数量有限,起点终点的石头不能拿走
using namespace std;
//L,N,M分别表示起点到终点的距离,起点和终点之间的岩石数,组委会至多移走的岩石数。
//nowrock记录当前的岩石序号,cnt表示记录已经移除的数量
int i,ii,l,n,m,nowrock,rock[100005],cnt;
//二分的左右边界和中间的值
int le,ri,mid;
int main(){
scanf("%d%d%d",&l,&n,&m);
for(int i=0;i<n;i++)
scanf("%d",&rock[i]);
le=1,ri=l;//设置二分查找的两个边界
//le=0也可
//输入的数据已经是升序排序的,不用再排序处理
while(le!=ri){//边界还能继续缩小,直到区间内只剩一个石头时停止处理
mid=(le+ri+1)/2;
//mid=(le+ri)/2;会TLE一部分
nowrock=cnt=0;
for(ii=0;ii<n;ii++){
if(l-rock[ii]<mid)break;//再跳时已超过终点,则不可拿走它和它后面的石头
if(rock[ii]-nowrock<mid)cnt++;//跳过石头
else nowrock=rock[ii];//贪心
}
/*
下面三行是重点
*/
cnt+=n-ii;//统计已经移除的数量
if(cnt<=m)le=mid;//如果移除的数量不够或者正好,就接着进行上述过程,改变区间左边界
else ri=mid-1;//否则改变区间右边界
}
printf("%d",le);
return 0;
}
P1080 [NOIP2012 提高组] 国王游戏
这个没做 累了
P1080 [NOIP2012 提高组] 国王游戏 https://www.luogu.com.cn/problem/P1080
知识点:贪心、高精度
贴一个解题链接
三、动态规划;递推/递归
P1002 [NOIP2002 普及组] 过河卒
P1002 [NOIP2002 普及组] 过河卒 https://www.luogu.com.cn/problem/P1002
状态转移方程 dp[i][j]=dp[i-1][j]+dp[i][j-1];
因为棋子可以向右或者向下,它到达一个点的前提是上一步在它的上面或左面
//x,y表示马的位置,m,n表示B点位置; a记录该点能否走,dp记录到某点的路线数量
long long x,y,m,n,a[25][25],dp[25][25];
void set(int x,int y) {//按马的行走规则进行搜索,标记棋子可以通过的点
a[x][y]=1;//马本身所在的位置是马到达了的,用1标记
//开始按马的行走方式,对它可以到达并拦截棋子的位置标记
//各个if中一定都是大于等于而非等于,因为各个点的下标都是非负数,要判断可到达的位置的下标是合法的
if(x-2>=0&&y-1>=0)
a[x-2][y-1]=1;
if(x+2>=0&&y-1>=0)
a[x+2][y-1]=1;
if(x-2>=0&&y+1>=0)
a[x-2][y+1]=1;
if(x+2>=0&&y+1>=0)
a[x+2][y+1]=1;
if(x-1>=0&&y-2>=0)
a[x-1][y-2]=1;
if(x-1>=0&&y+2>=0)
a[x-1][y+2]=1;
if(x+1>=0&&y-2>=0)
a[x+1][y-2]=1;
if(x+1>=0&&y+2>=0)
a[x+1][y+2]=1;
}
int main() {
//输入马的位置和目的地B点的位置
//注意对于棋盘,各行各列的下标都是从0开始
cin>>x>>y>>n>>m;
set(n,m);//标记B点周围棋子不被马拦截的点
dp[1][0]=1;//设置递推边界
//两个for的变量都从1开始才能保证自始至终i-1和j-1表达的下标是合法的(行列都从0开始标记,要控制它们是非负数)
for(int i=1; i<=x+1; i++)
for(int j=1; j<=y+1; j++){
//所以从循环开始,i表示的是i-1的位置,j表示的是j-1的位置
//相当于我们求解时把下标全都手动-1了
dp[i][j]=dp[i-1][j]+dp[i][j-1];//状态转移方程
if(a[i-1][j-1]==1)//如果这个点是马能达到的,那么这个点不能走,路径数归零
dp[i][j]=0;
}
//输出时要把前面所说的变动改回去
//dp[x+1][y+1]表达的是到dp[i][j]有多少条路
cout<<dp[x+1][y+1]<<endl;
return 0;
}
P1044 [NOIP2003 普及组] 栈
P1044 [NOIP2003 普及组] 栈 https://www.luogu.com.cn/problem/P1044
递归
#define maxn 1005
int f[maxn][maxn],n;
//递归
//f[x][y],下标 x表示队列里待排的数,y表示栈里的数,f[x][y]表示此时的情况的结果
inline int js(int x,int y){
if(f[x][y])//已经有结果的不用再重复求
return f[x][y];
if(x==0)//递归边界,也就是数字全部进栈了,只有1种情况
return 1;
if(y>0) //队列里有数字,栈有两种可能
/*栈空时,只能进入,所以队列里的数-1,栈里的数+1,即加上 f[x-1][y+1];
栈不空时,那么此时有出栈1个或者进1再出1个共两种种情况,分别加上 f[x-1][y+1] 和 f[x][y-1]
*/
/*下面两行顺序不能调换,否则结果有误*/
f[x][y]+=js(x,y-1); //这句可以直接写,是因为栈不空它才发生,栈空时y=0,y-1<0不合法,不会计算
f[x][y]+=js(x-1,y+1);//栈要么空要么不空,所以这个式子在两种情况都有,但是写一次就行
return f[x][y];
}
int main(){
cin>>n;
cout<<js(n,0);
return 0;
}
P1028 [NOIP2001 普及组] 数的计算
P1028 [NOIP2001 普及组] 数的计算 https://www.luogu.com.cn/problem/P1028
#define maxn 1005
int f[maxn],n;
inline int dg(int n){
if(n==1||n==0)//只有一个数字 n的数列是一个合法的数列
//n=1时的数列都只有一个包括它们本身的数列
//n=0是递归边界
return 1;
if(f[n])//已经有结果的不用再求
return f[n];
int ans=0;//统计结果的中间变量。
//两个合法数列 a,b不同当且仅当两数列长度不同或存在一个正整数 i≤∣a∣∣,使得 ai≠bi。
for(int j=1;j<=n/2;j++)//从短到长求数列 ,数列尾部新加的元素符合要求2
ans+=dg(j);
return f[n]=ans+1;//+1是因为尾部每次加一个数都生成一个新数列,每次上一行调用dg()时,j都会调用第一个或这一个return
}
int main(){
cin>>n;
dg(n);
cout<<dg(n);
return 0;
}
P1164 小A点菜
P1164 小A点菜 https://www.luogu.com.cn/problem/P1164
类似01背包的动态规划
//dp[i][j],i表示看过的菜,j表示状态下花费的钱
int dp[maxn][maxn],v[maxn],n,m;
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)
v[i]=read();
//开始支配钱包。一道道菜开始看
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
//每种菜只有一份+所有钱花光-->菜品从头到尾枚举,相当于01背包
if(j<v[i])//此时买不起这道菜
dp[i][j]=dp[i-1][j];//看过这道菜和看过这道菜之前的点餐状态是一样的
if(j==v[i])//刚好买得起?买!在看过上一道菜的基础上点餐数+1
dp[i][j]=dp[i-1][j]+1;
if(j>v[i])//买得起,买完还有剩余的钱
//下一行中当前状态下, 达到看过i道菜花费j元的状态。这个状态分两部分:看过i-1道菜花j元的状态一样可以达到(因为有余额),看过 i-1道菜花j-v[i]元的状态也能达到
dp[i][j]=dp[i-1][j]+dp[i-1][j-v[i]];
}
write(dp[n][m]);
return 0;
}
四、模拟+枚举
模拟:按题意的意思直接直接直接写代码。
P1618 三连击(升级版)
P1618 三连击(升级版) https://www.luogu.com.cn/problem/P1618
模拟+枚举
using namespace std;
//记录三个三位数各个位的变量;统计数字使用情况的数组;比例变量;标记变量
int i,j,l,n,m,x,y,z,d,e,f,g,h,k,num[9],a,b,c,flag,v;
int main() {
cin>>a>>b>>c;
/*下一行的判断针对新增的某测试数据。
题目描述的是数字1-9,但输入格式中并未对三个数限制,存在有0的情况
此时不满足题目要求,输出No!!!即可 */
if(a==0||b==0||c==0){
cout<<"No!!!"<<endl;
return 0;
}
//我们不知道输出结果可能有多少个,所以判断一组输出一组就可以了,也比较节省空间
//输出要求升序排列,那么从小到大判断就行
//输出的都是三位数,那么最大的数不超过999,最小的数不超过333
for(i=1; i<=3; i++)//开始枚举第一个数的百位、十位、个位
for(j=1; j<=9; j++)
for(l=1; l<=9; l++) {
x=100*i+10*j+l;//第一个数x
y=x*b/a;//第二个数
z=x*c/a;//第三个数
d=y/100;//第二个数的百十个位
e=(y%100)/10;
f=y%10;
g=z/100;//第三个数的百十个位
h=(z%100)/10;
k=z%10;
//把三个数的各个位数字,共9个,存入数组方便判断是否是1-9
num[0]=i;
num[1]=j;
num[2]=l;
num[3]=d;
num[4]=e;
num[5]=f;
num[6]=g;
num[7]=h;
num[8]=k;
//开始判断,从头到尾对数组元素两两比较。 相邻数字相同,或数字中有0,就不符合要求,赋值给标记变量
for(m=0; m<=8; m++)
for(n=m+1; n<=8; n++) {
if(num[m]==num[n]||num[m]==0||num[n]==0)
v=1;//标记变量之一,用于判断对组成的数字的约束,即9个数为1-9
}
if(v!=1&&z<=999){//最终输出的要求是9个数都用到了,而且最大的数是3位数
flag++;//统计生成的组数
cout<<x<<" "<<y<<" "<<z<<endl;//输出
}
v=0;//v归零
}
if(!flag)//如果一组也没生成,就输出无解
cout<<"No!!!"<<endl;
return 0;
}
P1149 [NOIP2008 提高组] 火柴棒等式
P1149 [NOIP2008 提高组] 火柴棒等式 https://www.luogu.com.cn/problem/P1149
模拟+枚举+递归
(有点枚举但不多)
using namespace std;
//match数组记录每个数字所需的火柴数,下标表示数字,数组元素表示对应下标数字需要的火柴
int match[10]={6,2,5,5,4,5,6,3,7,6},n,ans;
inline int solve(int x){//求摆一个数字需要多少根
int s=0;//用s记录摆x需要的火柴数量
if(x==0)//数字0可以直接返回match[0]的值
return match[0];
while(x>0){//x>0时,不管它有多少位,先用%求最后一位,s记录摆最后一位的火柴数,再用/把最后一位去掉
s+=match[x%10];
x=x/10;
}
return s;
}
//使用火柴最少的数字是1(2根) ;加号和等号一共需要4根
int main(){
cin>>n;
for(int i=0;i<=1111;i++)
for(int j=0;j<=1111;j++){
if(solve(i)+solve(j)+solve(i+j)+4==n)
++ans;
}
cout<<ans;
return 0;
}
五、排序
P1177 【模板】排序
P1177【模板】排序 https://www.luogu.com.cn/problem/P1177
不放代码了 排序有很多种 各有千秋
P1093 [NOIP2007 普及组] 奖学金
P1093 [NOIP2007 普及组] 奖学金 https://www.luogu.com.cn/problem/P1093
结构体的定义及应用+结构体排序规则
int n;
struct student{
int num;
int sum;
int chi;
int mat;
int eng;
}pui[305];
bool cmp(student a,student b){
if(a.sum>b.sum)
return 1;
else if(a.sum<b.sum)
return 0;
if(a.chi>b.chi)
return 1;
else if(a.chi<b.chi)
return 0;
if(a.num<b.num)
return 1;
else if(a.num>b.num)
return 0;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
pui[i].num=i;
cin>>pui[i].chi>>pui[i].mat>>pui[i].eng;
}
for(int i=1;i<=n;i++)
pui[i].sum=pui[i].chi+pui[i].mat+pui[i].eng;
sort(pui+1,pui+1+n,cmp);
for(int i=1;i<=5;i++){
cout<<pui[i].num<<' '<<pui[i].sum<<endl;
}
return 0;
}
P1116 车厢重组
P1116 车厢重组 https://www.luogu.com.cn/problem/P1116
模拟冒泡排序,并计算交换了多少次
也可以用其它排序方法实现
#define MAXN 100005
int n[MAXN],num,ans;
int main(){
cin>>num;
for(int i=1;i<=num;i++)
cin>>n[i];
for(int i=1;i<num;i++)
for(int j=i;j<=num;j++){
if(n[i]>n[j])
ans++;
}
cout<<ans;
return 0;
}
P1923 【深基9.例4】求第 k 小的数
P1923 【深基9.例4】求第 k 小的数 https://www.luogu.com.cn/problem/P1923
模板,二分法求第k小的数
#define MAXN 5000005
int x[MAXN],k,n;
void qsort(int l,int r){
int i=l,j=r,mid=x[(l+r)/2];
do{
while(x[j]>mid) j--;
while(x[i]<mid) i++;
if(i<=j){
swap(x[i],x[j]);
i++;
j--;
}
}while(i<=j);
if(k<=j) qsort(l,j);
else if(i<=k) qsort(i,r);
else{
printf("%d",x[j+1]);
}
return;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<=n;i++)
scanf("%d",&x[i]);
qsort(0,n-1);
return 0;
}