在 Kali 和 Ubuntu 上安装 Code Blocks

操作环境

Kali 2019.1

操作步骤

添加软件源:

root@kali:~# add-apt-repository ppa:damien-moore/codeblocks-stable

更新软件源:

root@kali:~# sudo apt-get update

安装 Code Blocks:

root@kali:~# sudo apt-get install codeblocks

启动 Code Blocks:

root@kali:~# codeblocks

之后, 可以参考我这篇博文对 Code Blocks 的外观进行优化, 文章地址:
http://zhaokaifeng.com/?p=1590

Kali Linux 忘记系统登录密码后的重置密码操作

前言

如果忘记了系统的登录密码, 最关键的需求就是重置 root 用户的登录密码, 之后使用 root 账户可以修改其他账户的密码. 因此, 本文就介绍一下在不知道 root 用户登录密码的情况下如何重置 root 账户的密码.

操作环境

Kali Linux 2019.1

操作步骤

启动 Kali Linux, 在启动了 GNU GRUB (GNU 引导) 界面时按 e 进入编辑模式.
之后找到如图 1 所示的位置, 把其中的 ro 改成 rw, 并删除 .gz 后面的 quite, 替换成 init=/bin/bash.

图 1
图 1

修改后如图 2 所示:

图 2
图 2

修改完成之后, 使用 Ctrl +X 组合键继续系统的启动进程, 此时是可以无密码直接进系统的, 如图 3:

图 3
图 3

在图 3 中所示的 # 提示符后面输入重置密码的 passwd 命令即可修改 root 用户的密码并使用修改后的密码正常登陆系统.

对使用 Z-BlogPHP 搭建的网站进行安全加固 (上)

前言

Z-BlogPHP 是一款常用的 Web 博客程序, 许多网站都是使用 Z-BlogPHP 搭建的. 今天, 我来结合自己目前所掌握的知识谈谈对使用 Z-BlogPHP 搭建的网站进行安全加固的方法. 希望本文能让更多的网站尽可能地远离安全威胁.

网站加固涉及很多方面, 有 Web 程序的加固, 中间件的加固, 代码运行环境的加固, 还有 Web 服务器的加固等, 本文只讨论对 Web 程序, 也就是 Z-BlogPHP 本身的加固.

操作环境

Z-BlogPHP 版本: Z-BlogPHP 1.5.2 Zero 正式版

加固步骤

保护登录入口

使用 Google 两步验证

两步验证相当于给登录过程加了两把锁, 即使我们网站的登录密码被恶意获取和利用, 攻击者也无法仅仅依靠该密码就成功登录网站后台. 两步验证的实现方式中常用的大致有两种:

  • 手机短信验证码
  • 虚拟 MFA

使用”手机短信验证码”的方式进行验证就是在登录的时候除了要输入用户名和密码之外还会要求输入一个由服务器发来的手机短信. 虚拟 MFA 的作用和手机短信验证码类似, 只不过虚拟 MFA 可以基于时间 (或者计数器) 生成验证码 (每 30 秒自动生成一个 6 位数字), 不需要联网即可运作.

目前可以提供虚拟 MFA 验证的软件有:

  • Authy
    官网: https://authy.com/
    Authy 提供的验证程序几乎覆盖了目前所有主流操作系统, 其支持的平台有 iOS, Android, Windows, macOS 以及 Chrome 浏览器.
  • 1Password
    官网: https://1password.com/
    1Password 是一个密码管理器, 也支持两步验证.
  • Microsoft Authenticator
    官网: https://www.microsoft.com/en-us/account/authenticator
    微软出品的两步验证程序.
  • Google 2-Step Verification
    官网: https://www.google.com/landing/2step
    Google 出品的两步身份验证器.

想要在 Z-BlogPHP 中使用两步验证, 首先需要在 Z-BlogPHP 的应用中心搜索”Google两步验证”找到相关插件, 插件 ID 是”LiangbuLogin”. 下载完成之后, 启用该插件.

下面我在手机上使用 Google 两步验证程序”Google 身份验证器”和 Z-BlogPHP 中的”Google两步验证”插件演示配置并启用两步验证的过程.

首先, 在 Z-BlogPHP 的管理后台中进入”Google两步验证”插件的配置界面并打开手机上的”Google 身份验证器”, 之后的操作过程如图 1 所示:

图 1
图 1

之后, 再登陆 Z-BlogPHP 的后台时会要求输入 Google 两步验证的验证码, 如图 2 所示.

图 2
图 2

如果进行两步验证的设备丢失, 可以在服务上删除如下目录及文件即可取消 Google 两步验证:

XX/zb_users/plugin/LiangbuLogin

另外, 需要注意的是, 如果设备支持的话, 请务必给进行两步验证的程序加上应用锁以尽可能避免未经授权的使用.

修改默认登录地址

Z-BlogPHP 默认的登录地址如下:

https://域名/zb_system/login.php

如果使用默认的登录地址的话, 就是把网站登录入口直接暴露在攻击者面前, 这是一个严重的不安全因素. 下面演示修改默认登录地址的方法.

首先将 zb_system/ 目录下的 login.php 文件修改成一个随机名称, 例如: ajdvirb6zmg9s7k2.php, 之后, 登录地址就变成了:

https://域名/zb_system/ajdvirb6zmg9s7k2.php

下一步, 在当前网站所使用的主题 (一般位于 xx/zb_users/theme/ 目录下) 下面找到 include.php 这个文件, 在其中加入如下 PHP 代码:

#修改默认登录地址
function zpnfewmcbxgsak(){
   global $zbp;
   $sfwidmsahdgv="ssocnf7sbsnxs";
   $dsdsuacskpqba="sdvn2ps3m2n";
   if($_GET[''.$sfwidmsahdgv.''] !== ''.$dsdsuacskpqba.'') {
      Redirect($zbp->host);
      die();
   }
}

之后, 同样在 include.php 这个文件中, 找到 function ActivePlugin_主题 ID(){} 函数, 在其中加入如下 PHP 代码:

Add_Filter_Plugin('Filter_Plugin_Login_Header','zpnfewmcbxgsak');

经过上述操作之后, 登录地址就变成了:

https://域名/zb_system/ajdvirb6zmg9s7k2.php?ssocnf7sbsnxs=sdvn2ps3m2n

上面地址中的任意一个字符不对都将无法打开登录页面.
如果忘记了登录地址, 只需要进入服务器, 找到上面所作的更改并组合成登录地址即可.

需要注意的一点是, Z-BlogPHP 默认会把后台登录地址显示在网站前台, 这个组件一定要去掉.

修改登录页面默认标题和内容

通常情况下, 登录页面都会有一些和登录相关的关键词, 例如”登录”, “用户登录”, “login”, “网站后台”, “用户名”和”密码”等. 如果登录界面被搜索引擎爬取到了, 那么攻击者通过类似”Google hack”这样的方式就有可能获取网站登录后台的地址. 因此, 有必要把登录页面的一些关键词修改成和”登录”以及”网站后台”无关的内容. 修改方法如下:

打开 zb_system/ 目录下的 login.php 文件(如果您按照上面的过程进行了修改, 那么此时, 这个文件的名称应该是 ajdvirb6zmg9s7k2.php, 具体名称以您的修改操作为准.), 进行如下修改:

修改用户名输入框的提示字符, 将:

<dd class="username"><label for="edtUserName"><?php echo $lang['msg']['username'] ?></label><input type="text" id="edtUserName" name="edtUserName" size="20" value="<?php echo GetVars('username', 'COOKIE') ?>" tabindex="1" /></dd>

修改成:

<dd class="username"><label for="edtUserName"><?php echo "hskVex" ?></label><input type="text" id="edtUserName" name="edtUserName" size="20" value="<?php echo GetVars('username', 'COOKIE') ?>" tabindex="1" /></dd>

修改密码输入框的提示字符, 将:

<dd class="password"><label for="edtPassWord"><?php echo $lang['msg']['password'] ?></label><input type="password" id="edtPassWord" name="edtPassWord" size="20" tabindex="2" /></dd>

修改成:

<dd class="password"><label for="edtPassWord"><?php echo "sFhfc" ?></label><input type="password" id="edtPassWord" name="edtPassWord" size="20" tabindex="2" /></dd>

修改页面标题, 将:

<title><?php echo $blogname . '-' . $lang['msg']['login'] ?></title>

修改成:

<title><?php echo "HFxs&3^k" . '-' . "pJ18&qxF" ?></title>

修改登录界面的图片标题和图片说明, 将:

<div class="logo"><img src="image/admin/none.gif" title="<?php echo htmlspecialchars($blogname) ?>" alt="<?php echo htmlspecialchars($blogname) ?>"/></div>

修改成:

<div class="logo"><img src="image/admin/none.gif" title="<?php echo "DshjFac@1A7%" ?>" alt="<?php echo "DsE2vshKw7" ?>"/></div>

修改”保持登录”提示字符, 将:

<dd class="checkbox"><input type="checkbox" name="chkRemember" id="chkRemember"  tabindex="98" /><label for="chkRemember"><?php echo $lang['msg']['stay_signed_in'] ?></label></dd>

修改成:

<dd class="checkbox"><input type="checkbox" name="chkRemember" id="chkRemember"  tabindex="98" /><label for="chkRemember"><?php echo "Qh1A2s$sAv%gC*" ?></label></dd>

修改登录按钮, 将:

<dd class="submit"><input id="btnPost" name="btnPost" type="submit" value="<?php echo $lang['msg']['login'] ?>" class="button" tabindex="99"/></dd>

修改成:

<dd class="submit"><input id="btnPost" name="btnPost" type="submit" value="<?php echo "HCL9" ?>" class="button" tabindex="99"/></dd>

记录访问登录页面的 IP 地址

对于记录 IP 地址这个功能, 我们可以使用第三方的网站统计服务来实现. 我们可以申请一个统计代码, 而且只将这个统计代码放在登录页面, 这样就可以记录到所有访问过登录界面的 IP 地址, 根据这些 IP 地址和登陆时间就可以大致判断网站的登录界面是否已经被攻击者掌握. 但是, 使用第三方服务也就意味着我们的后台登录地址将上传到第三方平台的服务器, 从而增大了后台登录地址可能受到攻击的攻击面, 在这里我们通过一段 PHP 代码实现对访问了登录页面的 IP 地址进行记录的功能.

打开 zb_system/ 目录下的 login.php 文件(如果您按照上面的过程进行了修改, 那么此时, 这个文件的名称应该是 ajdvirb6zmg9s7k2.php, 具体名称以您的修改操作为准.), 添加如下代码:

<?php
$ip = $_SERVER["REMOTE_ADDR"];
$var = "$ip;\n";

file_put_contents('ip.md', $var, FILE_APPEND | LOCK_EX);
?>

上述代码将在当前路径下创建 ip.md 文件并将带有当前访问者 IP 地址的变量 $var 的值以追加的方式 (使用参数 FILE_APPEND 实现内容追加) 写入到 ip.md 文件. 今后, 我们可以通过 ip.md 文件记录的内容获知是否有未授权的 IP 地址打开过登录界面.

使用强登录密码

这一点无需多言, 登录密码至少需要有 16 位, 且必须包括大小写字母, 数字和特殊字符并经常更换.

启用 Z-BlogPHP 自带的安全设置

在 Z-BlogPHP 的管理后台的”网站设置 / 全局设置”中开启”安全增强”并关闭”开发模式”, 如图 3:

图 3
图 3

在安装完必要的插件之后, 在 Z-BlogPHP 的管理后台的”应用中心 / 安全模式”中开启安全模式, Z-BlogPHP 建议我们不要使用可以进行文件管理, 数据库管理, 主题编辑和网站备份的插件, 如图 4:

图 4
图 4

如果要退出安全模式, 需要进入网站的服务器端, 手动删除以下文件:

/zb_users/data/appcentre_security_mode.php

之后, 在网站后台中进入”插件管理”, 停止”应用中心客户端”的运行. 今后只需要每隔特定时间检查一次更新或者在获知有新版本的程序发布时开启”应用中心客户端”进行一下更新即可.

防止恶意上传

由于我使用 Z-BlogPHP 搭建的网站中主要使用的图片是 .webp 格式, 除此之外基本没有上传过其他格式的文件, 因此可以在后台去掉对不常用的文件格式的上传支持. Z-BlogPHP 默认支持的上传格式如下:

jpg|gif|png|jpeg|bmp|psd|wmf|ico|rpm|deb|tar|gz|sit|7z|bz2|zip|rar|xml|xsl|svg|svgz|rtf|doc|docx|ppt|pptx|xls|xlsx|wps|chm|txt|pdf|mp3|mp4|avi|mpg|rm|ra|rmvb|mov|wmv|wma|swf|fla|torrent|apk|zba|gzba

我修改后的支持的上传格式如下 (在管理后台的”网站设置 / 全局设置”中可以进行修改):

webp|jpg|gif|png|jpeg|bmp

总之, 将允许上传的文件格式限制得越少越好.

删除不必要的文件

Z-BlogPHP 的安装程序在 zb_install 目录下, 安装完成后 zb_install 这个目录及其下的所有文件都可以直接删除以减少网站的受攻击面 (该删除操作不会影响网站的正常登陆和访问).

Solve the Problem of ownCloud: ‘No Keychain Service Available’ on Ubuntu 18.04

Operating Environment

Operating system: Ubuntu 18.04.2 LTS
ownCloud client version: 2.4.1

Problem Description

I use ownCloud in Ubuntu and I add one account to it. But whenever I reboot Ubuntu, ownCloud will tell me: “No keychain service available” and I’ll be asked to enter the password once, as show in the figure 1:

Figure 1
Figure 1

Once I enter the password, ownCloud will work properly. But once I restart Ubuntu Linux, that happens again and again. Passwords should be stored, but it didn’t.

Solve the Problem

I found a solution on Github through Bing and that link is HERE. It seems that I am not the only one who has encountered this problem.

The solution is very simple, installing:

sudo apt install libgnome-keyring0

Then, reboot Ubuntu Linux, after you log in to the system, you will be asked to enter a password once, only once, whether you restart Ubuntu or not, you will not be asked to enter ownCloud’s password again unless you exit ownCloud account.

This problem has been solved.

在 Windows 10 上优化 CodeBlocks 外观 (主题 + 字体)

操作环境

操作系统: Windows 10 64 位 中文家庭版
CodeBlocks 版本: Code::Blocks 17.12

操作说明

CodeBlocks 是一款优秀的 IDE, 但是其默认的主题实在是不能满足我对一个代码编译器最基本的需求: 不使用亮色主题.
CodeBlocks 默认的主题是白色的, 如图 1:

图 1
图 1

在写代码时, 编辑器通常都是全屏显示的, 白色的主题看久了眼睛是受不了的. 不过, 好在 CodeBlocks 官网上提供了暗色的主题可供选择, 本文就介绍一下通过 CodeBlocks 官网提供的主题代码更换 CodeBlocks 主题以及修改 CodeBlocks 默认字体的方法.

继续阅读“在 Windows 10 上优化 CodeBlocks 外观 (主题 + 字体)”

在 Debian 7 Linux 中将 PHP 5.4 升级到 PHP 5.6

操作环境

root@IronMan:~# cat /etc/issue
Debian GNU/Linux 7 \n \l

操作步骤

指定软件源, 编辑:

vim /etc/apt/sources.list.d/dotdeb.list

写入如下内容:

deb http://packages.dotdeb.org wheezy-php56 all
deb-src http://packages.dotdeb.org wheezy-php56 all

将上述软件源加入 apt key:

wget http://www.dotdeb.org/dotdeb.gpg -O- | apt-key add -

更新可用包列表:

aptitude update

升级 PHP 版本:

aptitude install php5-cli php5-fpm

更新日志:2019年03月25日

  1. 在”首页”一级标签下的”网络安全天天看”二级标签下新增如下网址:
序号名称URL
1Krebs on Securityhttps://krebsonsecurity.com/
  1. 将”WebSite”一级标签更名为”Server”.
  2. 在”数学”一级标签下的”博客”二级标签中新增如下网址:
序号名称URL
1陶哲轩的博客/Terence Tao’s Bloghttps://terrytao.wordpress.com/
2Matrix67http://www.matrix67.com/blog/
  1. 在”Server”一级标签下新增”SSH (Secure Shell)”二级标签, 并在该二级标签中新增如下网址:
序号名称URL
1PuTTYhttps://putty.org/
2SecureCRThttps://www.vandyke.com/products/securecrt/index.html
  1. 在”Server”一级标签下的”网站统计&分析”二级标签下新增如下网址:
序号名称URL
1ClustrMapshttp://clustrmaps.com/
  1. 将”AI”一级标签更名为”AI&Blockchain”.
  2. 在”AI&Blockchain”一级标签下新增”比特币 Ω Bitcoin”二级标签, 并在该二级标签下新增如下网址:
序号名称URL
1Statoshihttps://statoshi.info/
2Bitcoin.pagehttps://lopp.net/bitcoin.html

2017 年蓝桥杯 C 语言 B 组省赛第 2 题: 等差素数列

题目

标题:等差素数列

2,3,5,7,11,13,….是素数序列。
类似:7,37,67,97,127,157 这样完全由素数组成的等差数列,叫等差素数数列。
上边的数列公差为30,长度为6。

2004年,格林与华人陶哲轩合作证明了:存在任意长度的素数等差数列。
这是数论领域一项惊人的成果!

有这一理论为基础,请你借助手中的计算机,满怀信心地搜索:

长度为10的等差素数列,其公差最小值是多少?

注意:需要提交的是一个整数,不要填写任何多余的内容和说明文字。

题目分析

首先, 素数 (质数) 都是大于 1 的, 因此 1 不是素数, 这点在题目中也有说明.
本题的解题思路是这样的:
我们首先需要定义一个用于判断一个数是不是素数的函数, 下面这个函数就可以判断一个大于 2 的数是不是一个素数, 如果是素数就返回真:

bool f(int x){
    for(int i=2;i<x;i++){
        if(x%i==0){
            return 0;
        }
    }
    return 1;
}

这里需要注意的是, 如果把上面这个判断素数的函数写成下面这样是不对的, 因为如果一个数能够整除除了自身和 1 之外的其他数则可以断定该数不是素数, 但是如果一个数不能够整除除了自身和 1 之外的其他数中的一个, 是不能判断其是不是素数的, 错误的素数判断程序如下:

bool f(int x){
    for(int i=2;i<x;i++){
        if(x%!=0){
            return 1;
        }
    }
    return 0;
}

之后, 我们需要从素数的起始位置, 也就是数字 2 开始, 从小到大依次判断前 1 个数加 1 是不是素数, 如果不是素数且之前计算出来的数字个数小于 10, 那么第 2 个素数开始进行上面的逐个加 1 的操作, 直到第 99999 个数字 (这个数字不一定是素数, 如果不是素数不参加之前的运算) 为止, 如果不能以 1 为公差找到 10 个数字, 则之后还是从数字 2 开始逐个加 2 并判断是不是素数, 如果不是素数则从第二个素数开始逐个进行加 2 的操作, 直到第 99999 个数字为止, 如果不能以 2 为公差找到 10 个数字, 则之后从数字 2 开始进行加 3 的操作…直到第 99999 个数字为止.

如果计算之后找不到结果可以将上面提到的有关搜索范围适当扩大.

在具体的编程实现上有以下两种.

程序 1:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int a[99999];
bool f(int x){
    for(int i=2;i<x;i++){
        if(x%i==0){
            return 0;
        }
    }
    return 1;
}

int main(){

    for(int a=2;a<99999;a++){ //遍历首项
        for(int b=1;b<999;b++){ //遍历公差

//使用十层 if 判断寻找符合条件的数值, 能抵达第十层的说明满足条件, 输出之
            if(f(a)){
                if(f(a+b*1)){
                    if(f(a+b*2)){
                        if(f(a+b*3)){
                            if(f(a+b*4)){
                                if(f(a+b*5)){
                                    if(f(a+b*6)){
                                        if(f(a+b*7)){
                                            if(f(a+b*8)){
                                                if(f(a+b*9)){
                                                    cout<<b<<endl;
                                                }else{
                                                    continue;
                                                }
                                            }else{
                                                continue;
                                            }
                                        }else{
                                            continue;
                                        }
                                    }else{
                                        continue;
                                    }
                                }else{
                                    continue;
                                }
                            }else{
                                continue;
                            }
                        }else{
                            continue;
                        }
                    }else{
                        continue;
                    }
                }else{
                    continue;
                }
            }else{
                continue;
            }
        }
    }

    return 0;
}

程序 2:

#include<iostream>
#include<bits/stdc++.h>
#define MAX 999999
using namespace std;
int a[MAX];

bool f(int x){ //判断一个数字是不是素数的函数
    for(int i=2;i<x;i++){
        if(x%i==0){
            return 0;
            //如果能整除, 则表示这是一个非素数, 返回假 (0)
        }
    }
    return 1;
    //如果不能整除, 则表示这是一个素数, 返回真 (0)
}

int main(){

//遍历从 2 到 99999 之间的数字, 将其中的素数标记为 1
    for(int i=2;i<99999;i++){
        if(f(i)){
            a[i]=1;
        }
    }

    for(int delta=1;delta<999;delta++){
    //遍历从 1 到 999 之间的公差
        for(int i=2;i<99999;i++){
        //遍历数组 a[i]

            int step;
            for(step=0;step<10;step++){
            //限制只遍历数组 a[i] 中的 10 个数字

                //如果第 i+delta*step 个数字不是素数就退出
                if(a[i+delta*step]!=1){
                    break;
                }
            }

            //如果能完整的走完 10 步则表示找到了答案
            if(step==10){
                cout<<delta<<endl;
                return 0;
            }
        }
    }
    return 0;
}

本题的正确答案:
210

2016 年蓝桥杯 C 语言 B 组省赛第 3 题: 凑算式 (两种方法)

题目

凑算式

     B      DEF
A + --- + ------- = 10
​     C      GHI


(如果显示有问题,可以参见【图1.jpg】)


这个算式中A~I代表1~9的数字,不同的字母代表不同的数字。

比如:
6+8/3+952/714 就是一种解法,
5+3/1+972/486 是另一种解法。

这个算式一共有多少种解法?

注意:你提交应该是个整数,不要填写任何多余的内容或说明性文字。

图 1
图 1

题目分析

本题就是一个无重复全排列的问题, 在 C++ 中实现无重复全排列有以下两种主要方式:

  1. 通过 next_permutation() 函数实现全排列

next_permutation() 全排列函数只能对有序数列的数组进行全排列, 示例如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int main(){
    int a[3]={0,1,2};
    while(next_permutation(a,a+3)){
        cout<<a[0]<<a[1]<<a[2];
        cout<<endl;
    }
    return 0;
}
  1. 通过循环实现全排列

假设我们要对从 1 到 3 这个三个数字进行全排列, 那么我们可以使用如下的方式进行:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    for(int a=1;a<=3;a++){
        for(int b=1;b<=3;b++){
            if(b!=a){
                for(int c=1;c<=3;c++){
                    if(c!=a&&c!=b){
                    cout<<a<<" "<<b<<" "<<c<<endl;
                    }
                }
            }
        }
    }
    return 0;
}

另外, 在本题中需要注意的是, 本题给出的算式是存在除法的, 涉及除法就不得不考虑精度的问题. 为了解决精度的问题, 一方面我们可以使用将整型强制转换成 double 型的方式, 另一方面也可以使用将除法转换成乘法的方式进行计算.

根据上面的分析, 我们可以有两种方法计算本题.

方法一: 使用 C++ 中的 next_permutation() 函数计算, 程序如下:

#include<iostream>
#include<bits/stdc++.h> //万能头文件, 包含所有函数
using namespace std;
int main(){
    int a[9]={1,2,3,4,5,6,7,8,9};
    int sum=0;
    while(next_permutation(a,a+9)){
    //对数组a的前9个元素进行全排列, 也就是对全部元素进行全排列

        double sum2=(double)a[0]+(double)a[1]/a[2]+double(a[3]*100+a[4]*10+a[5])/(a[6]*100+a[7]*10+a[8]);
        if(sum2==10.0){
            sum++;
        }
    }
    cout<<sum<<endl;;
    return 0;
}

方法二: 使用九层 for 循环计算, 程序如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    int ans=0;
    for(int a=1;a<=9;a++){
        for(int b=1;b<=9;b++){
            if(b!=a){
                for(int c=1;c<=9;c++){
                    if(c!=a&&c!=b){
                        for(int d=1;d<=9;d++){
                            if(d!=a&&d!=b&&d!=c){
                                for(int e=1;e<=9;e++){
                                    if(e!=a&&e!=b&&e!=c&&e!=d){
                                        for(int f=1;f<=9;f++){
                                            if(f!=a&&f!=b&&f!=c&&f!=d&&f!=e){
                                                for(int g=1;g<=9;g++){
                                                    if(g!=a&&g!=b&&g!=c&&g!=d&&g!=e&&g!=f){
                                                        for(int h=1;h<=9;h++){
                                                            if(h!=a&&h!=b&&h!=c&&h!=d&&h!=e&&h!=f&&h!=g){
                                                                for(int i=1;i<=9;i++){
                                                                    if(i!=a&&i!=b&&i!=c&&i!=d&&i!=e&&i!=f&&i!=g&&i!=h){
                                                                        double sum2=(double)a+(double)b/c+double(d*100+e*10+f)/(g*100+h*10+i);
                                                                        if(sum2==10.0){
                                                                            ans++;
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

2016 年蓝桥杯 C 语言 B 组省赛第 2 题: 生日蜡烛 (三种解法)

题目

生日蜡烛

某君从某年开始每年都举办一次生日party,并且每次都要吹熄与年龄相同根数的蜡烛。

现在算起来,他一共吹熄了236根蜡烛。

请问,他从多少岁开始过生日party的?

请填写他开始过生日party的年龄数。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。

题目分析

这道题可以使用循环来解决. 根据实际情况, 首先假设”某君”的年龄没有超过100岁, 之后假设他从 0 岁开始过生日并吹蜡烛, 之后一直累加, 直到他吹蜡烛的总数为 236, 如果从 0 岁开始计算没有出现吹了 236 根蜡烛的情况, 那么就从 1 岁开始计算, 直到计算出吹了 236 根蜡烛的情况.

在具体的程序实现方式上有三种不同的解法.

第一种解法是, 第一层 for 循环用来遍历开始过生日的年龄, 第二层 for 循环用来遍历现在的年龄, 第三层 for 循环用来遍历从开始年龄到现在年龄之间吹的蜡烛的总数, 程序如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    for(int start=1;start<100;start++){
    //遍历开始过生日的年龄

        for(int now=1;now<100;now++){
        //遍历现在的年龄

            int sum=0;
            if(start<=now){
            //现在的年龄需要大于过去的年龄

                for(int i=start;i<=now;i++){
                //累加计算蜡烛总数
                    sum=sum+i;//注意这里要累加的变量是 i 不是 start
                }
                if(sum==236){
                //如果蜡烛总数为 236 则输出结果
                cout<<"开始的时间:"<<endl;
                cout<<start<<endl;
                cout<<"现在的时间:"<<endl;
                cout<<now<<endl;
                }
            }
        }
    }
    return 0;
}

第二种解法是, 第一层 for 循环用来遍历开始过生日的年龄, 第二层 for 循环用来遍历下一次过生日时的年龄并进行累加, 程序如下:

#include<iostream>
#include<stdio.h>
using namespace std;
int main(){
    for(int i=0;i<200;i++){ //i 为开始年龄
        int sum=0;
        //sum 要调用它的循环外最近的地方定义并初始化

        for(int j=i;j<200;j++){
        //j 为下次过生日时的年龄

            sum=sum+j;
            //计算吹蜡烛的总数

            if(sum==236){
                cout<<"开始的年龄是:"<<i<<endl;
                cout<<"现在的年龄是:"<<j<<endl;
            }
        }
    }
    return 0;
}

第三种解法是, 利用等差数列的求和公式进行计算.

等差数列的前 n 项和:

[latex]S_n=\frac{n(a_1+a_n)}{2}[/latex]

等差数列的第 n 项值:

[latex]a_n=a_1+(n-1)d[/latex]

等比数列的前 n 项和:

[latex]S_n=\frac{a_1(1-q^n)}{1-q}[/latex]

等比数列的第 n 项值:

[latex]a_n=a_1q^{(n-1)} [/latex]

根据等差数列的数学原理, 我们可以编写出如下计算程序:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    for(int i=1;i<=100;i++){
    //i 为数列的首项

        double sum=0;
        int a=0;
        int b=0;
        for(int j=1;j<=100;j++){
        //j 为数列递增的次数

            b=j+1;
            //目前数列有 b 项 (项数比递增的次数多 1)

            a=i+b-1;
            //计算本数列最后一项的值

            sum=b*(i+a)/2;
            //计算本数列的和

            /*每计算出一次 sum 的值都要立即进行一次判断
            判断语句不能放在本次循环的外面
            因为一个循环会遍历所有的数值之后才退出
            如果把判断语句放在循环的外面
            则不能使每次循环得出的数值都被判断*/
            if(sum==236){
            cout<<"开始过生日的年龄为:"<<i<<endl;
        }
        }

    }
    return 0;
}

在使用循环遍历的时候, 如果不能计算出我们想要的结果, 这个时候可以把循环的次数改小一些, 之后进行单步调试, 看看是不是和我们手算出来的结果一致.

2016 年蓝桥杯 C 语言 B 组省赛第 1 题: 煤球数目

题目

煤球数目

有一堆煤球,堆成三角棱锥形。具体:
第一层放1个,
第二层3个(排列成三角形),
第三层6个(排列成三角形),
第四层10个(排列成三角形),
….
如果一共有100层,共有多少个煤球?

请填表示煤球总数目的数字。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。

题目分析

本题是一个找规律的问题, 找到的规律如下:

第1层: 0+1=1
第2层: 1+2=3
第3层: 3+3=6
第4层: 6+4=10
……

可以看到, 规律就是上一层的煤球个数加上本层的层数就可以得到本层的煤球个数.

之后用编程实现, 代码如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int main(){
    int sum=0;
    int sum2=0;
    for(int i=1;i<=100;i++){
        sum=sum+i;
        sum2=sum2+sum;
    }
    cout<<sum2<<endl;
    return 0;
}

本题正确答案:
171700

2014 年蓝桥杯 C 语言 B 组省赛第 3 题: 李白打酒

题目

标题:李白打酒

话说大诗人李白,一生好饮。幸好他从不开车。

一天,他提着酒壶,从家里出来,酒壶中有酒2斗。他边走边唱:

无事街上走,提壶去打酒。
逢店加一倍,遇花喝一斗。

这一路上,他一共遇到店5次,遇到花10次,已知最后一次遇到的是花,他正好把酒喝光了。

请你计算李白遇到店和花的次序,可以把遇店记为a,遇花记为b。则:babaabbabbabbbb 就是合理的次序。像这样的答案一共有多少呢?请你计算出所有可能方案的个数(包含题目给出的)。

注意:通过浏览器提交答案。答案是个整数。不要书写任何多余的内容。

题目分析

关于方案数量的问题往往都是使用深搜. 使用递归实现深搜可以使我们不必关心遍历的具体过程, 只需要把题目中给出的条件转换成程序语言即可.
根据题目信息, 变量有”店”, “酒”和”花”, 因此我们的递归函数必须包含这三个变量, 即:

void f(int dian, int hua, int jiu)...

根据”逢店加一倍,遇花喝一斗。”可以得出如下两个对函数 f() 自身进行调用的语句:

if(遇到店){
  f(dian-1,hua,jiu*2);
}

if(遇到花){
  f(dian,hua-1,jiu-1);
}

递归的边界是遇到了 5 次店, 10 次花, 并且最后酒喝光了, 即:

dian==0&&hua==0&&jiu==0

但是上面的分析过程 (对递归边界的定义) 是存在错误的. 我们根据上面的分析过程可以写出下面这个 (错误的) 程序:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int ans=0;

void f(int dian, int hua, int jiu){
        if(dian>0&&jiu>=0){
            f(dian-1,hua,jiu*2);
        }

        if(hua>0&&jiu>=1){
            f(dian,hua-1,jiu-1);
        }

        if(dian==0&&hua==0&&jiu==0){
            ans++;
        }

    }

int main(){

    f(5,10,2);
    cout<<ans<<endl;

    return 0;
}

上面这个程序的运行结果是:
27

“27”看上去也”像是”一个正确答案. 但是, 如果我们仔细分析题目就会发现, 上面这个程序没有满足题目中给出的下面这个条件:

已知最后一次遇到的是花,他正好把酒喝光了

也就是说, 李白最后一次遇到的是花, 而且在刚遇到的花的时候, 他的酒壶里还剩 1 斗酒, 随后”遇花喝一斗”把酒壶里面的酒都喝光了. 至此, 李白一共遇到了 5 次店, 10 次花, 并且喝完了全部的酒.

但是, 在上面的程序中, 计算的仅仅是李白”遇到了 5 次店, 10 次花, 并且喝完了全部的酒”, 存在李白最后连续遇到了两次花, 每次喝一斗, 最后把酒喝完的情况, 也存在李白最后连续遇到了四次花, 每次喝一斗, 最后把酒喝完的情况(题目中没有提到酒壶容量的上限).

为了满足这个”已知最后一次遇到的是花,他正好把酒喝光了”的条件, 我们先把最后一次遇到花并喝一斗酒的情况减去, 这样只需要计算李白遇到 5 次店, 9 次花, 并剩下了 1 斗酒有多少种情况就可以了, 正确的程序如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int ans=0; //定义全局变量用于计数

void f(int dian, int hua, int jiu){

/*模拟李白遇到店的情况
若要遇到店, 则剩下的店的数量必须大于 0
遇到店的时候, 剩下的花的数量不变但必须不为负数
遇到店之前不能把酒壶里面的酒喝成负数*/
        if(dian>0&&jiu>=0&&hua>=0){
            f(dian-1,hua,jiu*2);
        }

/*模拟李白遇到花的情况
若要遇到花, 则剩下的花的数量必须大于 0
遇到花的时候, 剩下的店的数量不变但必须不为负数
遇到花之前酒壶里面的酒至少要剩 1 斗以便于遇到花时喝*/        
        if(hua>0&&jiu>=1&&dian>=0){
            f(dian,hua-1,jiu-1);
        }

        if(dian==0&&hua==0&&jiu==1){
            ans++;
        }

    }

int main(){
    f(5,9,2);
    cout<<ans<<endl;
    system("pause");
    return 0;
}

本题正确答案:
14

2014 年蓝桥杯 C 语言 B 组省赛第 2 题: 切面条

题目

标题:切面条

一根高筋拉面,中间切一刀,可以得到2根面条。

如果先对折1次,中间切一刀,可以得到3根面条。

如果连续对折2次,中间切一刀,可以得到5根面条。

那么,连续对折10次,中间切一刀,会得到多少面条呢?

答案是个整数,请通过浏览器提交答案。不要填写任何多余的内容。

题目分析

本题其实可以不需要使用编程的方式解决, 这是一个数列找规律的问题. 对于找规律的问题需要记住的一点就是要手算出尽可能多的项, 这样找出来的规律才比较可靠 (题目中已经给出了数列的前三个值, 如果最终的规律可以靠前三个数列导出的话, 那么这道题就没什么考点了, 因此至少需要计算出数列中第 4 个数的值).

数列的规律可以从以下几个方面寻找:

  • 两个数值间的关系是否和两个数之间的差值有关系, 差值的变化是否具有某种规律, 例如呈指数增长的差值;
  • 前两个数的和 (或者差, 积, 商) 是否可以得出其后的数.

通过使用 Windows 系统中的”画图”工具绘制出前 4 种情况(如果时间充裕的话可以在得出结果后绘制第 5 种情况以验证对规律的猜测是否正确), 如图 1

图 1
图 1

由此, 我们可以得到关于面条个数的这样一个数列:

2, 3, 5, 9

接着我们可以得到这样一个规律:

2+0=2 (对折 0 次)
2+1=3 (对折 1 次)
3+2=5 (对折 2 次)
5+4=9 (对折 3 次)

进而得到:

2+2^0=3 (对折 1 次)
3+2^1=5 (对折 2 次)
5+2^2=9 (对折 3 次)

规律找到这里, 我们可以使用程序进行之后的计算, 程序如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    int a=2;
    int ans=2;

    for(int i=0;i<=9;i++){
            ans=ans+pow(a,i);
    }
    cout<<ans<<endl;
    return 0;
}

程序运行结果:

1025

如果不知道 C/C++ 用于求次方的函数是什么, 也可以使用循环代替, 下面的程序同样可以计算出最终结果:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int main(){
    int ans=5;

    for(int i=2;i<=9;i++){
        int b=2;
        int a=2;
        for(int j=1;j<i;j++){
            b=b*a; //a 在循环的过程中必须始终为 2
        }
            ans=ans+b;
    }
    cout<<ans<<endl;
    return 0;
}

程序运行结果:

1025

在运行程序之前可以先设置断点进行单步调试, 看看程序运行的前几步是不是和我们手动计算出来的结果 (和规律) 一致.

另外, 正如本文开头所说的, 本题可以不通过编程的方式解决, 因为这可以看做是一个数学问题, 就是找数列的规律. 但是为什么我们上面找到数列的规律后还需要用程序计算呢? 因为上面得到的规律中, 等号左边的变量有两个, 每一步的计算都需要上一步的结果作为支撑 (当然, 也可以不编程, 直接借助系统中的计算器逐步计算, 这个方法也可以用于对程序计算结果的验证), 每一步的计算都不是独立的, 这样的规律显然不适合手算, 之前找到的规律如下:

2+2^0=3 (对折 1 次)
3+2^1=5 (对折 2 次)
5+2^2=9 (对折 3 次)

为了方便手算, 我们必须想办法去掉一个变量, 于是, 就有了下面这个规律:

1+2^0+2^0=3 (对折 1 次)
1+2^1+2^1=5 (对折 2 次)
1+2^2+2^2=9 (对折 3 次)

进而得到:

1+2*2^0=3 (对折 1 次)
1+2*2^1=5 (对折 2 次)
1+2*2^2=9 (对折 3 次)

进而又可得到:

1+2^1=3 (对折 1 次)
1+2^2=5 (对折 2 次)
1+2^3=9 (对折 3 次)

在上面的规律中, 对每次对折的求解都不依赖上一次对折得出的相关数值, 变量只有一个, 即对折的次数, 因此可以通过一次计算就得出对折 10 次后再在中间切一刀能够得到的面条个数, 计算过程与结果为:

1+2^10=1025

本题需要注意的一点是, 得到两根面条的时候 (第 1 次切的时候) 对折的次数是 0 次.
本题的关键是正确地找出数列中的多个数值, 如果只找出前三个数值则本题很可能会得出错误的结果.

2014 年蓝桥杯 C 语言 B 组省赛第 1 题: 啤酒和饮料

题目

标题:啤酒和饮料

啤酒每罐2.3元,饮料每罐1.9元。小明买了若干啤酒和饮料,一共花了82.3元。

我们还知道他买的啤酒比饮料的数量少,请你计算他买了几罐啤酒。

注意:答案是一个整数。请通过浏览器提交答案。

不要书写任何多余的内容(例如:写了饮料的数量,添加说明文字等)。

题目分析

这里使用使用循环暴力破解即可, 根据啤酒和饮料的价格以及一共花费了八十多块钱可以大致估计, 啤酒的数量不会超过 50 罐, 饮料的价格不会超过 60 罐, 由于有啤酒和饮料两个, 因此用两个嵌套的 for 循环对其进行遍历即可.

下面先来看一个有问题的程序.

下面这个程序在逻辑上是符合的, 但是无法运行出结果:

#include <iostream>
using namespace std;
int main(){
    for (int i=1; i<=50; i++){
        for (int j=1; j<=60; j++){
            if((i<j)&&(i*2.3+j*1.9==82.3)){
                cout<<i<<" "<<j<<endl;
            }
        }
    }
    return 0;
}

无法出结果的原因是, 如果参与运算的有浮点数, 那个其运算结果是不能用于比较是否相等的 (“==”两边不能是浮点数), 因为浮点数的精度不同可能导致两个本来相同的浮点数不相等.

正确的比较方法是计算两个数的差值, 如果差值小于一个极小的数就表明这两个数字是相等的, 正确的程序如下:

#include<iostream>
#include<cmath>
using namespace std;
int main(){
    for (int i=1; i<=50; i++){
        for (int j=1; j<=60; j++){
            if((i<j)&&abs((i*2.3+j*1.9) - 82.3)<0.0000000000001){
                //abs()库函数用于求绝对值
                cout<<i<<" "<<j<<endl;
            }
        }
    }
    return 0;
}

运行结果:

11 30

当然, 本题也可以通过将题目中给出的数据都扩大 10 倍, 将浮点类型转换成 int 类型之后再计算, 程序如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int main(){
    for(int pj=1;pj<60;pj++){
        for(int yl=1;yl<60;yl++){
            if(pj<yl&&pj*23+yl*19==823){
                cout<<"啤酒:"<<pj<<endl;
                cout<<"饮料:"<<yl<<endl;
            }
        }
    }
    return 0;
}

运行结果:

啤酒:11
饮料:30

其中 11 是啤酒的罐数且满足啤酒的罐数小于饮料的罐数 (可以在得出结果后使用 PC 中的计算器验证一下).
本题正确答案:
11

2013 年蓝桥杯 C 语言 B 组省赛第 3 题: 第39级台阶

题目

题目标题: 第39级台阶

小明刚刚看完电影《第39级台阶》,离开电影院的时候,他数了数礼堂前的台阶数,恰好是39级!

站在台阶前,他突然又想着一个问题:

如果我每一步只能迈上1个或2个台阶。先迈左脚,然后左右交替,最后一步是迈右脚,也就是说一共要走偶数步。那么,上完39级台阶,有多少种不同的上法呢?

请你利用计算机的优势,帮助小明寻找答案。

要求提交的是一个整数。
注意:不要提交解答过程,或其它的辅助说明文字。

题目分析

本题的正确答案是:

51167078

这里涉及递归, 斐波那契数列和动态规划, 可以使用深度优先搜索 (DFS) 的思想解决问题.

首先说一下深度优先搜索. 深搜的原则就是对每一个可能的路径都深入访问直到不能继续深入访问为止, 而且每个节点只访问一次. 深搜的应用条件是上一步的走法和下一步的走法的规则是一样的且该问题具备边界以结束深搜.

首先可以简化一下这个问题, 去掉题目中要求的”走偶数步”的限制. 之后, 剩下的问题就是每次上一个或者两个台阶, 一共有多少种走法, 第一步有两种可能(走一个台阶或者走两个台阶), 随后的每走过一步的下一步也都是有两种走法(走一个台阶或者走两个台阶).

假设函数 f(n) 可以计算在上面的条件下走完 n 阶台阶会有多少种走法, 则:

走完 1 个台阶之后 (走了 1 步), 剩余的走法有 f(n-1) 种;

走完 2 个台阶之后 (走了 1 步), 剩余的走法有 f(n-2) 种.

结束条件有两个, 一个是恰好走完了 39 个台阶, 另一个是走到了第 40 个台阶(正着走的情况下, 只剩下一个台阶时却迈了两步)或者走到了第 -1 个台阶(倒着走的情况下, 只剩下一个台阶的时候却迈了两步).

正着走: 可以认为是从楼梯下面往上面走;
倒着走: 可以认为是从楼梯上面往下面走;
正着走和倒着走的效果是一样的 (例如当”楼梯”是水平的”斑马线”的时候).

使用递归实现深搜的函数大致形式如下:

递归函数f(...){
    if(...){ //本层递归函数结束条件 1
        /* code */
    }else if (...) { //本层递归函数结束条件 2
        /* code */
    }else{
        递归函数f(...)//可能的步骤 1
        递归函数f(...)//可能的步骤 2
    }
}

在具体使用递归解决深搜问题的时候, 不同的思路和方法的最终实现方式会有些差别, 具体情况可以参考如下 4 个程序 (每个程序都可以独立解决”第39级台阶”这个问题).

程序 1 如下:

#include<iostream>
#include<stdio.h>
using namespace std;
int ans; //用于保存所有可能的走法
void f(int n, int step){//n: 剩下的阶梯数, step: 已走的步数

/*剩下的台阶数是负数 (如果最后只剩下一个台阶却走了两步
会导致产生剩下负数个台阶的情况), 这是不可能发生的,
因此退出 f() 函数.*/
    if(n<0){//判断边界条件, 函数出口 1

        return;
/*在 void 函数中使用"return"可以出发函数的强制结束,
类似于循环结构中的 "break", 在这里"return"用于退出
本层深度遍历, 回到上一个未被遍历的节点继续之后的深度遍历.
*/

    }
    if(n==0&&step%2==0){//判断边界条件, 函数出口 2
        ans++;
        return;
    }

/*尝试每一种可能的结果("n-1"和"n-2")并触发
下一步的递归操作 ("n-1,step+1"和"n-2,step+1")
*/
    f(n-1,step+1); //下一步可能的走法 1
    f(n-2,step+1); //下一步可能的走法 2
}
int main(){
    f(39,0);
    cout << ans << endl;
    return 0;
}

上面的程序是倒着计算(倒着走楼梯)的, 也可以按照正着计算(正着走楼梯)的方法写程序, 程序 2 如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int sum=0;

void f(int n, int step){

        if(n==39&&step%2==0){
            sum++;
            return;
        }

        if(n>39){
            return;
        }

        f(n+1,step+1);
        f(n+2,step+1);

    }

int main(){
    f(0,0);
    cout<<sum<<endl;
    return 0;
}

为了使逻辑上更清晰一些, 可以对上面的程序 2 做以下修改, 修改后得到的程序 3 如下:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int sum=0;

void f(int n, int step){

        if(n==39&&step%2==0){
            sum++;
            return;
        }else if(n>39){
            return;
        }else{
            f(n+1,step+1);
            f(n+2,step+1);
        }

    }

int main(){
    f(0,0);
    cout<<sum<<endl;
    return 0;
}

当然, 递归函数除了可以使用没有返回值的 “void f()” 来定义, 也可以使用有返回值的”int f()”来定义, 例如程序 4:

#include<iostream>
#include<bits/stdc++.h>
using namespace std;

int sum=0;

int f(int n, int step){

        if(n==39&&step%2==0){
            sum++;
            return sum;
        }else if(n>39){
            return -1;
        }else{
            f(n+1,step+1);
            f(n+2,step+1);
            return 0;
        }

    }

int main(){
    f(0,0);
    cout<<sum<<endl;
    return 0;
}