组合数的计算
组合数定义及计算公式如下图
为避免中间结果溢出,采用约分的方法,利用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.局部变量也是放在堆栈段的,栈溢出不一定是递归调用太多,也可能是局部变量太大。