算法竞赛入门经典(第二版)-刘汝佳-第七章 暴力求解法 例题(7/15)

文章目录

  • 说明
  • 例题
    • 例7-1 UVA 725 除法
    • 例7-2 UVA 11059 最大乘积
    • 例7-3 UVA 10976 分数拆分
    • 例7-4 UVA 524 素数环
    • 例7-5 UVA 129 困难的串
    • 例7-6 UVA 140 带宽
    • 例7-7 UVA 1354 天平难题(未尝试)
    • 例7-8 UVA 10603 倒水问题(未尝试)
    • 例7-9 UVA 1601 万圣节后的早晨(未尝试)
    • 例7-10 UVA 11212 编辑书稿(未尝试)
    • 例7-11 UVA 12325 宝箱
    • 例7-12 UVA 1343 旋转游戏(未尝试)
    • 例7-13 UVA 1374 快速幂计算(未尝试)
    • 例7-14 UVA 1602 网格动物(未尝试)
    • 例7-15 UVA 1603 破坏正方形(未尝试)

说明

本文是我对第七章15道例题的练习总结,建议配合紫书——《算法竞赛入门经典(第2版)》阅读本文。
另外为了方便做题,我在VOJ上开了一个contest,欢迎一起在上面做:第七章例题contest
如果想直接看某道题,请点开目录后点开相应的题目!!!

例题

例7-1 UVA 725 除法

题意
输入正整数n,按从小到大的顺序输出所有形如abcde/fghij = n的表达式,其中a~j恰好为数字0~9的一个排列(可以有前导0),2≤n≤79。
思路
枚举0~9的所有排列?没这个必要。只需要枚举fghij就可以算出abcde,然后判断是否所有数字都不相同即可。不仅程序简单,而且枚举量也从10!=3628800降低至不到1万,而且当abcde和fghij加起来超过10位时可以终止枚举。由此可见,即使采用暴力枚举,也是需要认真分析问题的。
代码

#include 
#include 
#include 
#include 
using namespace std;

int n;
int arr[2][5];
int ans;

void get_arr(int a[], int num)
{
    for (int i = 4; i > 0; i --) {
        a[i] = num%10;
        num /= 10;
    }
    a[0] = num;
}

bool check()
{
    int cnt[10] = {0};
    for (int i = 0; i < 2; i ++) {
        for (int j = 0; j < 5; j ++) {
            if (arr[i][j] >= 10 || ++cnt[arr[i][j]] > 1) return false;
        }
    }
    return true;
}

void print()
{
    for (int i = 0; i < 5; i ++)
        printf("%d", arr[0][i]);
    printf(" / ");
    for (int i = 0; i < 5; i ++)
        printf("%d", arr[1][i]);
    printf(" = %d\n", n);
}

int main(void)
{
    int t = 0;
    while (cin >> n && n) {
        if (t++) puts("");
        ans = 0;
        for (int y = 1234; y <= 50000; y++) {
            int x = y * n;
            get_arr(arr[0], x);
            get_arr(arr[1], y);
            if (check()) {
                ans++;
                print();
            }
        }
        if (!ans) printf("There are no solutions for %d.\n", n);
    }

    return 0;
}

例7-2 UVA 11059 最大乘积

题意
输入n个元素组成的序列S,你需要找出一个乘积最大的连续子序列。如果这个最大的乘积不是正数,应输出0(表示无解)。1≤n≤18,-10≤Si≤10。
样例输入:
3
2 4-3
5
2 5 -1 2 -1
样例输出:
8
20
思路
连续子序列有两个要素:起点和终点,因此只需枚举起点和终点即可。由于每个元素的绝对值不超过10且不超过18个元素,最大可能的乘积不会超过1018,可以用long long存储。

这个题还可以用DP来做,时间复杂度将从O(N^2)降成O(N)。当然这是后话了。
代码

#include 
#include 
#include 
#include 
using namespace std;

const int N = 18;

int main(void)
{
    int t = 0;
    int n, a[N];

    while (scanf("%d", &n) != EOF && n) {
        for (int i = 0; i < n; i ++)
            scanf("%d", &a[i]);

        long long res = a[0];
        for (int i = 0; i < n; i ++) {
            long long mult = 1;
            for (int j = i; j < n; j ++) {
                mult *= a[j];
                res = max(mult, res);
            }
        }
        if (res < 0) res = 0;
        printf("Case #%d: The maximum product is %lld.\n\n", ++t, res);
    }

    return 0;
}

例7-3 UVA 10976 分数拆分

题意
输入正整数k,找到所有的正整数x≥y,使得1/k=1/x+1/y.
思路
既然要求找出所有的x、y,枚举对象自然就是x、y了。可问题在于,枚举的范围如何?从1/12=1/156+1/13可以看出,x可以比y大很多。难道要无休止地枚举下去?当然不是。由于x≥y,有1/x≤1/y,因此1/k-1/y≤1/y,即y≤2k。这样,只需要在2k范围之内枚举y,然后根据y尝试计算出x即可。
代码

#include 
#include 
#include 
#include 
#include 
using namespace std;

int main(void)
{
    int k, x, y;

    while (scanf("%d", &k) != EOF) {
        int cnt = 0;
        typedef pair P;
        vector

res; for (y = k+1; y <= 2*k; y ++) { x = k*y; if (x % (y-k) == 0) { x /= (y-k); cnt ++; res.push_back(P(x, y)); } } printf("%d\n", cnt); for (int i = 0; i < cnt; i ++) printf("1/%d = 1/%d + 1/%d\n", k, res[i].first, res[i].second); } return 0; }


例7-4 UVA 524 素数环

题意
输入正整数n,把整数1, 2, 3,…, n组成一个环,使得相邻两个整数之和均为素数。输出时从整数1开始逆时针排列。同一个环应恰好输出一次。n≤16。
思路
如果最坏情况下的枚举量很大,应该使用回溯法而不是生成-测试法。
代码

#include 
#include 
#include 
using namespace std;

int isPrime(int n)
{
    for (int i=2; i<=sqrt(n); i++)
    {
        if (n%i == 0)
            return 0;
    }
    return 1;
}

int p[40];

void initPrime()
{
    int i;
    p[1] = 0;
    for (i=2; i<32; i++)
    {
        if (isPrime(i))
            p[i] = 1;
        else
            p[i] = 0;
    }
}

void printPrime()
{
    int i;
    for (i=1; i<=39; i++)
        printf("%d ", p[i]);
    printf("\n");
}

int b[17][17];

void initBeside()
{
    int i, j;
    for (i=1; i<17; i++)
    {
        b[i][0] = 0;
        for (j=1; j<17; j++)
        {
            if (j!=i && p[i+j])
                b[i][++b[i][0]] = j;
        }
    }
}

void printBeside()
{
    int i, j;
    for (i=1; i<=6; i++)
    {
        int tmp = b[i][0];
        for (j=1; j<=tmp; j++)
            printf("%d ", b[i][j]);
        printf("\n");
    }
}

int n;
int v[17];
int num[17];

void initSet()
{
    memset(v, 0, sizeof(v));
    v[1] = 1;
    num[1] = 1;
}

void printCircle()
{
    int i;
    for (i=1; i n)
            break;
        if (!v[tmp])
        {   
            num[k+1] = tmp;
            v[tmp] = 1;
            set(k+1);
            v[tmp] = 0;
        }
    }
}

int main(void)
{
    initPrime(); 
    //printPrime();
    initBeside();
    //printBeside();

    int count = 0;
    while (scanf("%d", &n) != EOF)
    {   
        if (count) printf("\n");
        printf("Case %d:\n", ++count);
        initSet();
        set(1);
    }

    return 0;
}

例7-5 UVA 129 困难的串

题意
如果一个字符串包含两个相邻的重复子串,则称它是“容易的串”,其他串称为“困难的串”。例如,BB、ABCDACABCAB、ABCDABCD都是容易的串,而D、DC、ABDAB、CBABCBA都是困难的串。
输入正整数n和L,输出由前L个字符组成的、字典序第k小的困难的串。例如,当L=3时,前7个困难的串分别为A、AB、ABA、ABAC、ABACA、ABACAB、ABACABA。输入保证答案不超过80个字符。
思路
基本框架不难确定:从左到右依次考虑每个位置上的字符。因此,问题的关键在于如何判断当前字符串是否已经存在连续的重复子串。例如,如何判断ABACABA是否包含连续重复子串呢?一种方法是检查所有长度为偶数的子串,分别判断每个字串的前一半是否等于后一半。尽管是正确的,但这个方法做了很多无用功。还记得八皇后问题中是怎么判断合法性的吗?判断当前皇后是否和前面的皇后冲突,但并不判断以前的皇后是否相互冲突——那些皇后在以前已经判断过了。同样的道理,我们只需要判断当前串的后缀,而非所有子串。
提示:在回溯法中,应注意避免不必要的判断,就像在八皇后问题中那样,只需判断新皇后和之前的皇后是否冲突,而不必判断以前的皇后是否相互冲突。
代码

#include 
#include 
#include 
#include 
#include 
using namespace std;

int m, k;
int cnt;

void print_hard(string s)
{
    int n = s.size();
    for (int i = 0; i < n; i++) {
        if (i % 4 == 0) {
            if (i % 64 != 0)
                printf(" ");
            else if (i)
                printf("\n");
        }
        printf("%c", s[i]);
    }
    printf("\n%d\n", n);
}

void find_hard(string s)
{
    int n = s.size();
    for (int i = 0; i < k; i++) {
        if (cnt >= m) break;
        char c = i+'A';
        bool flag = true;
        for (int j = 1; j <= (n+1)/2; j++) {
            if (s.substr(n-j+1)+c == s.substr(n+1-j*2, j)) {
                flag = false;
                break;
            }
        }
        if (flag) {
            if (++cnt == m) {
                print_hard(s+c);
                return;
            }
            find_hard(s+c);
        }
    }
}

int main(void)
{
    while (scanf("%d%d", &m, &k), m || k) {
        cnt = 0;
        find_hard("");
    }

    return 0;
}

例7-6 UVA 140 带宽

题意
给出一个n(n≤8)个结点的图G和一个结点的排列,定义结点i的带宽b(i)为i和相邻结点在排列中的最远距离,而所有b(i)的最大值就是整个图的带宽。给定图G,求出让带宽最小的结点排列.
思路
如果不考虑效率,本题可以递归枚举全排列,分别计算带宽,然后选取最小的一种方案。能否优化呢?和八皇后问题不同的是:八皇后问题有很多可行性约束(feasibility constraint),可以在得到完整解之前避免扩展那些不可行的结点,但本题并没有可行性约束——任何排列都是合法的。难道只能扩展所有结点吗?当然不是。
可以记录下目前已经找到的最小带宽k。如果发现已经有某两个结点的距离大于或等于k,再怎么扩展也不可能比当前解更优,应当强制把它“剪”掉,就像园丁在花园里为树修剪枝叶一样,也可以为解答树“剪枝(prune)”。
除此之外,还可以剪掉更多的枝叶。如果在搜索到结点u时,u结点还有m个相邻点没有确定位置,那么对于结点u来说,最理想的情况就是这m个结点紧跟在u后面,这样的结点带宽为m,而其他任何“非理想情况”的带宽至少为m+1。这样,如果m≥k,即“在最理想的情况下都不能得到比当前最优解更好的方案”,则应当剪枝。
提示:在求最优解的问题中,应尽量考虑最优性剪枝。这往往需要记录下当前最优解,并且想办法“预测”一下从当前结点出发是否可以扩展到更好的方案。具体来说,先计算一下最理想情况可以得到怎样的解,如果连理想情况都无法得到比当前最优解更好的方案,则剪枝。
代码

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

vector neigh[26];
int n;
int node[8];

int resd = 26;
vector vmin;

void choose(int m, int a[], bool used[], int pos[], int curd)
{
    if (m == n) {
        if (curd < resd) {
            resd = curd;
            vmin.clear();
            for (int i = 0; i < n; i++)
                vmin.push_back(a[i]);
        }
    }
    for (int i = 0; i < n; i++) {
        int k = node[i];
        if (!used[k]) {
            int maxd = curd;
            for (int j = 0; j < neigh[k].size(); j++) {
                int r = neigh[k][j];
                if (used[r])
                    maxd = max(maxd, abs(m - pos[r]));
            }
            if (maxd < resd) {
                a[m] = k;
                used[k] = 1;
                pos[k] = m;
                choose(m+1, a, used, pos, maxd);
                used[k] = 0;
            }
        }
    }
}

int main(void)
{
    char ch;
    while ((ch = getchar()) != '#') {
        for (int i = 0; i < 26; i++)
            neigh[i].clear();
        bool used[26];
        memset(used, 0, sizeof(used));
        do {
            int i = ch-'A';
            used[i] = 1;
            getchar();
            while ((ch = getchar()) != ';' && ch != '\n') {
                int j = ch-'A';
                neigh[i].push_back(j);
                neigh[j].push_back(i);
                used[j] = 1;
            }
            if (ch == '\n') break;
        } while (ch = getchar());
        n = 0;
        for (int i = 0; i < 26; i++) {
            if (used[i]) node[n++] = i;
        }

        int a[8], pos[26];
        memset(used, 0, sizeof(used));
        resd = 26;
        choose(0, a, used, pos, 0);

        for (int i = 0; i < n; i++)
            printf("%c ", vmin[i]+'A');
        printf("-> %d\n", resd);
    }

    return 0;
}

例7-7 UVA 1354 天平难题(未尝试)

题意

思路

代码



例7-8 UVA 10603 倒水问题(未尝试)

题意

思路

代码



例7-9 UVA 1601 万圣节后的早晨(未尝试)

题意

思路

代码



例7-10 UVA 11212 编辑书稿(未尝试)

题意

思路

代码



例7-11 UVA 12325 宝箱

题意
你有一个体积为N的箱子和两种数量无限的宝物。宝物1的体积为S1,价值为V1;宝物2的体积为S2,价值为V2。输入均为32位带符号整数(实际上是非负整数)。你的任务是计算最多能装多大价值的宝物。例如,n=100,S1=V1=34,S2=5,V2=3,答案为86,方案是装两个宝物1,再装6个宝物2。每种宝物都必须拿非负整数个。
思路
最容易想到的方法是:枚举宝物1的个数,然后尽量多拿宝物2。这样做的时间复杂度为O(N/S1),当N和S1相差非常悬殊时效率很低。当然,如果N/S2很小时可以改成枚举宝物2的个数,所以这个方法不奏效的条件是:S1和S2都很小,而N很大。
幸运的是,S1和S2都很小时,有另外一种枚举法B:S2个宝物1和S1个宝物2的体积相等,而价值分别为S2V1和S1V2。如果前者比较大,则宝物2最多只会拿S1-1个(否则可以把S1个宝物2换成S2个宝物1);如果后者比较大,则宝物1最多只会拿S2-1个。不管是哪种情况,枚举量都只有S1或者S2。
这样,就得到了一个比较“另类”的分类枚举算法:
当N/S1比较小时枚举宝物1的个数,时间复杂度为O(N/S1),否则,当N/S2比较小时枚举宝物2的个数,时间复杂度为O(N/S2),否则说明S1和S2都比较小,执行枚举法B,时间复杂度为O(max{S1, S2})。

万万没有想到看起来很简单的一个题我提交了近十次才AC。总结一下自己犯的错误:

  1. 输入语句scanf("%d", &t)误加了while循环,结果一直超时;
  2. 没有注意到答案范围可能超过int表示范围,也就是说ans应该定义成long long;
  3. 我的代码中还有几个变量或表达式需要用long long定义或强制转换,如LL i = 0 以及 (LL)s1v2 < (LL)s2v1。

代码

#include 
#include 
#include 
#include 
using namespace std;

typedef long long LL;

int main(void)
{
    int t, n, s1, v1, s2, v2;
    LL ans;
    scanf("%d", &t);
    for (int c = 1; c <= t; c ++) {
        scanf("%d%d%d%d%d", &n, &s1, &v1, &s2, &v2);
        ans = 0;
        if (n/s1 < 65536 || n/s2 < 65536) { // n/s1 或 n/s2较小
            if (s1 < s2) { swap(s1, s2); swap(v1, v2); } //修正的结果是s1大
            for (LL i = 0; i*s1 <= n; i++) 
                ans = max(ans,  i*v1 + ((n-i*s1) / s2) * v2);
        } else { // s1, s2都很小,并且n很大
            if ((LL)s1*v2 < (LL)s2*v1) { swap(s1, s2); swap(v1, v2); }
            for (LL i = 0; i < s2; i++)
                ans = max(ans,  i*v1 + ((n-i*s1) / s2) * v2);
        }

        printf("Case #%d: %lld\n", c, ans);
    }

    return 0;
}

例7-12 UVA 1343 旋转游戏(未尝试)

题意

思路

代码



例7-13 UVA 1374 快速幂计算(未尝试)

题意

思路

代码



例7-14 UVA 1602 网格动物(未尝试)

题意

思路

代码



例7-15 UVA 1603 破坏正方形(未尝试)

题意

思路

代码



你可能感兴趣的:(算法竞赛入门经典)