算法竞赛入门经典(第2版)笔记--第4章

本文是《算法竞赛入门经典(第2版)》第4章的读书笔记,主要内容包括组合数的计算方法及其避免溢出的技巧。通过分析UVa题目,如Ancient Cipher, Hangman Judge, The Dole Queue, Message Decoding和Spreadsheet Tracking,探讨了模拟和优化解题策略。此外,总结了本章的重要提示,如typedef结构体定义、中间结果溢出的处理、四舍五入的方法、调用栈的工作原理以及栈溢出和局部变量的影响。" 87732589,1382182,离散傅立叶变换在图像处理中的应用与解析,"['图像处理', '傅立叶变换', '离散傅立叶', '数学公式', '信号处理']

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

组合数的计算

组合数定义及计算公式如下图
这里写图片描述
为避免中间结果溢出,采用约分的方法,利用n!/m!=(m+1)(m+2)…(n-1)n
同时运用小技巧:当m小于n-m时,把m变成n-m

long long C(int n,int m){
    if (m<n-m) m=n-m;
    long long ans=1;
    for (int i=m+1;i<=n;i++)
        ans*=i;
    for (int i=1;i<=n-m;i++)
        ans/=i;
    return ans;
}

例4-1 古老的密码 (Ancient Cipher, UVa1339)

对两个字符串中26个字符出现的次数进行统计,并排序,若排序过后的结果相同则为YES

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main(){
    char s[2][105];
    int cnt[2][26];
    while (cin>>s[0]>>s[1]){
        int flag=0;
        memset(cnt,0,sizeof(cnt));
        for (int i=0;i<2;i++){
            int len=strlen(s[i]);
            for (int j=0;j<len;j++)
                cnt[i][s[i][j]-'A']++;  //统计各个字母出现次数
            sort(cnt[i],cnt[i]+26);  //按出现次数排序
        }
        for (int i=0;i<26;i++){
            if (cnt[0][i]!=cnt[1][i]){
                cout<<"NO"<<endl;
                flag=1;
                break;
            }
        }
        if (!flag)
            cout<<"YES"<<endl;      
    }
    return 0;
} 

例4-2 刽子手的游戏 (Hangman Judge, UVa489)

用一个数组标记单词中出现的字母并统计所需猜中的字符数,用cntr和cntw两个变量分别记录猜中的次数和猜错的次数进行模拟。

#include <iostream>
#include <cstring>
using namespace std;
int main(){
    int t;
    char s1[105],s2[105];
    int a[26];
    while (cin>>t && t!=-1){
        cin>>s1>>s2;
        cout<<"Round "<<t<<endl;
        memset(a,0,sizeof(a));
        int len1=strlen(s1);
        int cnt1=len1;
        for (int i=0;i<len1;i++){
            if (a[s1[i]-'a'])   
                cnt1--;    //若出现重复的字母,则所需猜中的字符数减1 
            a[s1[i]-'a']++;
        }
        int len2=strlen(s2);
        int cntr=0,cntw=0;
        for (int i=0;i<len2;i++){
            if (a[s2[i]-'a']){
                a[s2[i]-'a']=0;
                cntr++;
            }
            else
                cntw++;
            if (cntr==cnt1 || cntw==7)
                break;
        }
        if (cntr==cnt1)
            cout<<"You win."<<endl;
        else if (cntw>=7)
            cout<<"You lose."<<endl;
        else
            cout<<"You chickened out."<<endl;
    }
    return 0;
}

例4-3 救济金发放 (The Dole Queue,UVa133)

此题另可参考书中go函数,将顺时针和逆时针走合并,增加一个步长的参数(为1或-1),详细写法见书p82。

#include <iostream>
#include <cstring>
using namespace std;
int main(){
    int a[20];
    int n,k,m,p,q,left;
    while (cin>>n>>k>>m && n){
        memset(a,0,sizeof(a));
        for (int i=1;i<=n;i++)
            a[i]=1;
        p=0; q=n+1; left=n;
        while (left){
            int cnt1=0,cnt2=0;
            while (cnt1<k){
                p++;
                if (p>n) p=1;
                if (a[p]) cnt1++;
            }
            while (cnt2<m){
                q=q-1;
                if (q==0) q=n;
                if (a[q]) cnt2++;
            }
            if (p==q){
                printf("%3d",p);
                left--;
                a[p]=0;
            }
            else{
                printf("%3d%3d",p,q);
                left-=2;
                a[p]=0; a[q]=0;
            }
            if (left) printf(",");
        }
        printf("\n");
    }
    return 0;   
}

例4-4 信息解码 (Message Decoding, UVa213) (重要)

此题采用了书上提供的解法:用(len,value)这个二元组表示一个编码,其中len是编码长度,value是编码对应的十进制数,用codes[len][value]保存这个编码所对应的字符。此题的一个难点在于读取和处理输入数据,为此编写了3个函数,其中的读取处理方法对其他类似题目的情况由借鉴意义(如跨行读取字符)。

#include <stdio.h>
#include <string.h>
int code [8][1<<8];
//跨行读取字符 
int readchar(){
    while (true){
        int ch=getchar();
        if (ch!='\n' && ch!='\r')   return ch;  //一直读到非换行符位置 
    }
}
int readint(int k){
    int v=0;
    while (k--){
        v=v*2+readchar()-'0';
    }
    return v;
}
int readcodes(){
    memset(code,0,sizeof(code));
    code[1][0]=readchar();  //直接调到下一行开始读取,如果输入已经结束,会都到EOF 
    for (int len=2;len<=7;len++)
        for (int v=0;v<(1<<len)-1;v++){
            char c=getchar();
            if (c==EOF) return 0;
            if (c=='\n' || c=='\r') return 1;
            code[len][v]=c;
        }
    return 1;
}
int main(){
    while (readcodes()){
        while (true){
            int len=readint(3);
            //printf("len=%d\n",len);
            if (len==0) break;
            while (true){
                int v=readint(len);
                if (v==(1<<len)-1)  break;
                putchar(code[len][v]);
            }
        }
        printf("\n");
    }
    return 0;
}

例4-5 追踪电子表格中的单元格(Spreadsheet Tracking, UVa512)

此题的思路是将所有操作保存,然后对于每个查询重新执行每个操作,不需要模拟整个表格的变化,只需关注所查询的单元格的位置变化。定义一个Command结构体处理输入的命令,可简化程序。

#include <iostream>
#include <string.h>
using namespace std;

struct Command{
    char c[5];
    int r1,c1,r2,c2;
    int num;
    int x[20]; 
} cmd[500];

int main(){
    int r,c,n,t,kase=0;
    while (cin>>r>>c && r!=0){
        cin>>n;
        for (int i=0;i<n;i++){
            cin>>cmd[i].c;
            if (cmd[i].c[0]=='E'){
                cin>>cmd[i].r1>>cmd[i].c1>>cmd[i].r2>>cmd[i].c2;
            }
            else{
                cin>>cmd[i].num;
                for (int j=0;j<cmd[i].num;j++){
                    cin>>cmd[i].x[j];
                }
            }
        }
        cin>>t;
        int row,col;
        if (kase)   cout<<endl;
        cout<<"Spreadsheet #"<<++kase<<endl;
        for (int i=0;i<t;i++){
            cin>>row>>col;
            cout<<"Cell data in ("<<row<<","<<col<<") ";
            int flag=0;
            for (int j=0;j<n;j++){
                if (cmd[j].c[0]=='E'){
                    if (cmd[j].r1==row && cmd[j].c1==col){
                        row=cmd[j].r2;  col=cmd[j].c2;
                    }  
                    else if (cmd[j].r2==row && cmd[j].c2==col){  //不加else的话会交换两次,导致交换无效 
                        row=cmd[j].r1;  col=cmd[j].c1;
                    }
                }
                if (cmd[j].c[0]=='D'){
                    int *p;
                    if (cmd[j].c[1]=='R') p=&row; 
                    else p=&col;
                    int cur=*p;   //用cur存储当前的行/列值,因为之后增删操作会改变 
                    for (int k=0;k<cmd[j].num;k++){
                        if (cmd[j].x[k]==cur){
                            cout<<"GONE"<<endl;
                            flag=1; 
                            break;
                        }
                        else if (cmd[j].x[k]<cur)   (*p)--;
                    }
                }
                if (cmd[j].c[0]=='I'){
                    int *p;
                    if (cmd[j].c[1]=='R') p=&row;
                    else p=&col;
                    int cur=*p;
                    for (int k=0;k<cmd[j].num;k++){
                        if (cmd[j].x[k]<=cur)   (*p)++;
                    }
                }
                if (flag) break;
                //cout<<"-->("<<row<<","<<col<<")"<<endl;
            }
            if (!flag) cout<<"moved to ("<<row<<","<<col<<")"<<endl;
        }
    }
    return 0;
}

(本章习题部分暂只选取了其中3道完成,其余习题待之后空闲时间回来解决)

习题4-2 正方形(Squares,UVa201)

#include <iostream>
#include <string.h>
using namespace std;
int main(){
    int n,m,a,b,kase=0;
    char c;
    int H[10][10],V[10][10];
    while (cin>>n>>m){
        memset(H,0,sizeof(H));
        memset(V,0,sizeof(V));
        for (int i=0;i<m;i++){
            cin>>c>>a>>b;
            if (c=='H')
                H[a][b]=1;
            else
                V[b][a]=1;
        }
        if (kase)
            cout<<endl<<"**********************************"<<endl<<endl;
        cout<<"Problem #"<<++kase<<endl<<endl;
        int sum=0;
        for (int i=1;i<n;i++){
            int cnt=0;
            for (int row=1;row<=n-i;row++)
                for (int col=1;col<=n-i;col++){
                    int flag=1;
                    for (int j=col;j<col+i;j++)
                        if (H[row][j]==0 || H[row+i][j]==0)
                            flag=0;
                    for (int j=row;j<row+i;j++)
                        if (V[j][col]==0 || V[j][col+i]==0)
                            flag=0;
                    cnt+=flag;
                }
            if (cnt)
                cout<<cnt<<" square (s) of size "<<i<<endl;
            sum+=cnt;       
        }
        if (sum==0)
            cout<<"No completed squares can be found."<<endl;   
    }
    return 0;
} 

习题4-3 黑白棋(Othello,UVa220)
典型的模拟题,需要关注很多细节不然容易出错。寻找合理的棋子放置位置时需要依次遍历八个方向,变更棋盘的时候应考虑到不止一个方向可能出现夹住棋子的情况,而不能只变更一个方向就停止。此外需注意下棋方轮换的处理以及输出格式。(写的代码略繁琐,日后可再进一步简化)

#include <iostream>
#include <string.h>
using namespace std;
char a[10][10];     //棋盘 
int lm[10][10];     //记录可以放置棋子的位置 
char player,opponent;    
int cntb,cntw;  //统计棋盘中黑棋和白棋的个数 

bool boundaryCheck(int curr,int curc){
    //边界检查 
    if (curr<1 || curr>8 || curc<1 || curr>8)
        return false;
    else
        return true;
}

void roleChange(){
    //下棋状态交换 
    char temp=player;
    player=opponent;
    opponent=temp;
}

void change(int row,int col,int rd,int cd){
    //根据方向更改棋盘 
    int flag;
    if (player=='B')
        flag=1;
    else
        flag=-1;
    a[row][col]=player;
    while(true){
        row+=rd; col+=cd;
        if (!boundaryCheck(row,col) || a[row][col]==player)
            break;
        a[row][col]=player;
        cntb+=flag;
        cntw-=flag;
    }
}

bool isLegal(int row,int col){
    //判断当前在位置放置棋子是否合理,若合理则按规则更改棋盘 
    int flag=0;
    for (int rm=-1;rm<=1;rm++)
        for (int cm=-1;cm<=1;cm++){
            int currow=row,curcol=col;
            if (rm==0 && cm==0)
                    continue;
            int cnt=0;
            while (true){
                currow+=rm;
                curcol+=cm;
                if (!boundaryCheck(currow,curcol) || a[currow][curcol]!=opponent)
                    break;
                cnt++;
            }
            if (boundaryCheck(currow,curcol) && a[currow][curcol]==player && cnt){
                change(row,col,rm,cm);
                flag=1;
            }
        }
    if (flag)
        return true;
    else
        return false;
}

void legalMove(){
    //找出当前棋盘状态下所有可以放置棋子的位置 
    memset(lm,0,sizeof(lm));
    for (int row=1;row<=8;row++)
        for (int col=1;col<=8;col++){
            if (a[row][col]==player){
            //  printf("(%d,%d) %c %c\n",row,col,player,opponent);
                for (int rm=-1;rm<=1;rm++)
                    for (int cm=-1;cm<=1;cm++){
                        int currow=row,curcol=col;
                        if (rm==0 && cm==0)
                            continue;
                        int cnt=0;
                        while (true){
                            currow+=rm;
                            curcol+=cm;
            //              printf("(%d,%d)=%c ",currow,curcol,a[currow][curcol]);
                            if (!boundaryCheck(currow,curcol) || a[currow][curcol]!=opponent)
                                break;
                            cnt++;
                        }
            //          cout<<endl;
                        if (boundaryCheck(currow,curcol) && a[currow][curcol]=='-' && cnt)
                            lm[currow][curcol]=1;
                    }
            }
        }
    int flag=0;
    for (int i=1;i<=8;i++)
        for (int j=1;j<=8;j++){
            if (lm[i][j]){
                if (flag)
                    printf(" ");
                printf("(%d,%d)",i,j);
                flag=1;
            }
        }
    if (flag)   
        cout<<endl;
    else
        cout<<"No legal move."<<endl;
}

int main(){
    int n;
    char cmd[5];
    //freopen("D:\\input.txt","r",stdin);
    //freopen("D:\\output.txt","w",stdout);
    cin>>n;
    while (n--){
        memset(a,' ',sizeof(a));
        cntb=0; cntw=0;
        for (int i=1;i<=8;i++){
            for (int j=1;j<=8;j++){
                cin>>a[i][j];
                if (a[i][j]=='B') cntb++;
                if (a[i][j]=='W') cntw++;
            }
        }
        cin>>player;
        if (player=='B')
            opponent='W';
        else
            opponent='B';
        while (gets(cmd) && cmd[0]!='Q'){
            if (cmd[0]=='L')
                legalMove();
            if (cmd[0]=='M'){
                int r=cmd[1]-'0',c=cmd[2]-'0',rd,cd;
                if (!isLegal(r,c)){
                    roleChange();
                    isLegal(r,c);
                }
                if (player=='B')
                    cntb++;
                else
                    cntw++;
                printf("Black - %2d White - %2d\n",cntb,cntw);
                roleChange();
            }
        }
        for (int i=1;i<=8;i++){
            for (int j=1;j<=8;j++)
                cout<<a[i][j]; 
            cout<<endl;
        }
        if (n)
            cout<<endl; 
    }
    return 0;
}

习题4-4 骰子涂色 (Cubic painting, UVa253)
此题解题关键在于,每次固定顶面不动骰子顺时针旋转侧面,这样一共会得到6*4=24种可能的序列,依次列举所有情况即可判断两个骰子是否等价。其中技巧是用一个二维数组记录不同顶面时各面所对应的数字。
注:char * strncpy(char *dest, const char *src, int n)
把src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中,并返回dest。如果n小于src的长度,只是将src的前n个字符复制到dest的前n个字符,不自动添加’\0’,也就是结果dest不包括’\0’,需要再手动添加一个’\0’。

#include <iostream>
#include <string.h>
using namespace std;

//列出不同面作为顶面时各面对应的数字 
int a[6][6]={{0,1,2,3,4,5},{1,0,3,2,5,4},{2,0,1,4,5,3},{3,0,4,1,5,2},{4,0,2,3,5,1},{5,1,3,2,4,0}};

int main(){
    char s[15],s1[8],s2[8];
    while (scanf("%s",s)!=EOF){
        strncpy(s1,s,6);
        s1[6]='\0';
        strncpy(s2,s+6,7);
        int flag=0;
        for (int i=0;i<6;i++){
            //依次用不同的面作为顶面 
            char c[8];
            for (int j=0;j<6;j++)
                c[j]=s1[a[i][j]];
            c[6]='\0';
            for (int j=0;j<4;j++){
                //顶面和底面固定不动,顺时针旋转 
                char temp=c[1];
                c[1]=c[2];
                c[2]=c[4];
                c[4]=c[3];
                c[3]=temp;
                if (strcmp(c,s2)==0) 
                    flag=1;
            }
        }
        if (flag) 
            cout<<"TRUE"<<endl;
        else
            cout<<"FALSE"<<endl;
    }
    return 0;
}
第4章重要提示摘要总结

1.在程序中可以定义结构体有时会很有用,往往用”typedef struct { 域定义;} 类型名; 的方式定义,之后可以像原生数据类型一样进行使用。

2.即使最终答案在所选择的数据类型范围之内,计算的中间结果仍然可能溢出,对复杂表达式进行化简有时不仅能减少计算量,还能减少甚至避免中间结果溢出。

3.四舍五入:floor(sqrt(n)+0.5)

4.调用栈描述的是函数之间的调用关系,它由多个栈帧组成,每个栈帧对应着一个未完成的函数。栈帧中保存了该函数的返回地址和局部变量。

5.不要滥用指针。如 int *t; 它在赋值之前是不确定的,如果这个“不确定的值”所代表的内存单元恰好是能写入的,那么这段程序将正常工作,如果它是只读的,程序可能会崩溃。

6.以数组为参数调用函数时,实际上只有数组首地址传递给了函数,需要另一个参数表示元素个数。除了把数组首地址本身作为实参外,还可以利用指针加减法把其他元素的首地址传递给函数。

7.调用栈所在的段成为堆栈段,有自己的大小,不能被越界访问,否则会出现段错误。每次递归调用都需要往调用栈里增加一个栈帧,久而久之就可能越界,这种情况叫做栈溢出(Stack Overflow)。

8.局部变量也是放在堆栈段的,栈溢出不一定是递归调用太多,也可能是局部变量太大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值