建模的第一步是选择合理的数据结构来表达问题。实际生活中的问题千变万化,但是数据结构就只有固定的几种。选择合理的数据结构来表达问题,也就是建立模型。
建模的第二步是分析模型的内在规律,并用变成语言表达其规律。
面试题43:n个骰(tou二声)子的点数
把n个骰子扔在地上,所有的骰子朝上一面的点数和为s。输入n,打印出s的所有的可能值出现的概率
leetcode链接 https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/class Solution { public: vector
twoSum(int n) { vector result; if (n < 1) { return result; } // 为了好交替调用,将两个矩阵定义为二维矩阵 int* sum[2]; sum[0] = new int[n*6 + 1]; // 所有可能和的总数,每位存放的就是该位对应和出现次数 sum[1] = new int[n*6 + 1]; int flag = 0; for(int i = 0; i < n*6 + 1; i++) { sum[0][i] = 0; sum[1][i] = 0; } for(int i = 1; i <= 6; i++) { sum[0][i] = 1; // 只有一个骰子的情况 } for(int k = 2; k <= n; k++) { for(int i = 0; i < k; i++) // k个骰子最小和是k,因此小于k是不可能了 { sum[1-flag][i] = 0; } for(int i = k ; i < n*6 + 1; i++) // 当点数大于k,考虑当前骰子点数和=上一次骰子点数和-1....前一次骰子点数和-6 { sum[1-flag][i] = 0; for(int j = 1; j <= 6 && j <= i; j++) { sum[1-flag][i] += sum[flag][i-j]; } } flag = 1 -flag; } double total = pow(6, n); for(int i = n; i <= 6*n; i++) { result.push_back(sum[flag][i] / total); } return result; } };
解题思路:
思路1:全排列。建立数组,每次排列后sum=n,则数组第n位+1,最后循环数组,计算每一个非零位概率。全排列用递归,会超时。这里递归思路用的是每次固定数组中一位,更正规一点的思想是用回溯。定义一个栈,每次循环push进当前位固定好的元素,递归调用n-1函数,函数退出后将push进元素pop出来。
class Solution {
public:
int all = 0;
void fullPer(int* arr, map& sum, int index, int n)
{
if (index >= n - 1)
{
all++;
int tmp = 0;
for(int i = 0; i < n; i++)
{
// cout << arr[i] << endl;
tmp = tmp + arr[i];
}
sum[tmp]++;
return;
}
for(int i = 1; i < 7; i++)
{
arr[index+1] = i;
fullPer(arr, sum, index+1, n);
}
return;
}
vector twoSum(int n) {
vector result;
map sum;
int *arr = new int[n];
for(int i = 1; i < 7; i++)
{
arr[0] = i;
fullPer(arr, sum, 0, n);
}
map::iterator iter = sum.begin();
while(iter != sum.end())
{
result.push_back(iter->second / double(all));
iter++;
}
return result;
}
};
思路2:用两个数组来存储骰子点数的每一个总数出现次数。在一次循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。在下一次循环中,我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1,n-2...n-6的和,所以我们把另一个数组的n位设置为n-1,n-2...n-6的和。最后被循环的数组就代表全排列后的结果。
class Solution {
public:
vector twoSum(int n) {
vector result;
if (n < 1)
{
return result;
}
// 为了好交替调用,将两个矩阵定义为二维矩阵
int* sum[2];
sum[0] = new int[n*6 + 1]; // 所有可能和的总数,每位存放的就是该位对应和出现次数
sum[1] = new int[n*6 + 1];
int flag = 0;
for(int i = 0; i < n*6 + 1; i++)
{
sum[0][i] = 0;
sum[1][i] = 0;
}
for(int i = 1; i <= 6; i++)
{
sum[0][i] = 1; // 只有一个骰子的情况
}
for(int k = 2; k <= n; k++)
{
for(int i = 0; i < k; i++) // k个骰子最小和是k,因此小于k是不可能了
{
sum[1-flag][i] = 0;
}
for(int i = k ; i < n*6 + 1; i++) // 当点数大于k,考虑当前骰子点数和=上一次骰子点数和-1....前一次骰子点数和-6
{
sum[1-flag][i] = 0;
for(int j = 1; j <= 6 && j <= i; j++)
{
sum[1-flag][i] += sum[flag][i-j];
}
}
flag = 1 -flag;
}
double total = pow(6, n);
for(int i = n; i <= 6*n; i++)
{
result.push_back(sum[flag][i] / total);
}
return result;
}
};
面试题44:扑克牌的顺子
从扑克牌中随机抽5张牌,判断是不是顺子。扑克牌中2-10为数字本身,A为1,J为11,Q为12,K为13。而大王小王可以看做任意数字。
leetcode链接 https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof/class Solution { public: bool isStraight(vector
& nums) { if (nums.size() == 0) { return false; } int max = nums[0]; int min = nums[0]; int zeroNum = 0; vector tmpVec; for(int i = 0; i < nums.size(); i++) { int tmp; if (nums[i] == 0) { zeroNum++; continue; } if (nums[i] == 'A') { tmp = 1; } else if (nums[i] == 'J') { tmp = 11; } else if (nums[i] == 'Q') { tmp = 12; } else if (nums[i] == 'K') { tmp = 13; } else { tmp = nums[i]; } if (find(tmpVec.begin(), tmpVec.end(), tmp) == tmpVec.end()) { tmpVec.push_back(tmp); } else{ return false; } if (tmp < min || min == 0) { // cout << "min: " << min << endl; min = tmp; } if (tmp > max || max == 0) { max = tmp; } } // cout << "max: " << max << " min: " << min << " zeroNum: " << zeroNum << " size: " << nums.size() << endl; return (max - min + 1) <= nums.size(); } };
解题思路:由于大小王可以是任意数,将其设为0,方便操作 1)数组排序 2)记录数组中0的个数,记录数组最大值-非零最小值的差 + 1=数组长度+n,若n=0的个数,则为顺子,否则,不是。
转变下思路,感觉不排序也可以,遍历,最大,最小值,无相等元素,若最大值-最小值+1 <= 数组长度,则为顺子。
面试题45:圆圈中最后剩下的数字(约瑟夫环)
0,1....n-1这n个数字排成一个圆圈,从数字0开始,每次从圆圈中删除第m个数字,求圆圈中剩余的最后一个数字。
leetcode链接 https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/class Solution { public: int lastRemaining(int n, int m) { if (n == 0) { return 0; } if (m == 1) // 总是删除当前节点,最后获得的一定是最后一个节点 { return n - 1; } // 递归写法,效率低 // return (lastRemaining(n-1, m) + m) % n; int* result = new int[n]; result[0] = 0; for(int i = 1; i < n; i++) { result[i] = (result[i-1] + m) % (i+1); // cout << i << " " << result[i] << endl; } return result[n-1]; } };
解题思路:
解法一:经典解法。用环形链表模拟操作过程。超时,心塞。注意m=1的情况。
class Solution {
public:
struct ListNode{
int val;
ListNode* next;
ListNode(int val) {this->val = val; next = NULL;}
};
int lastRemaining(int n, int m) {
if (n == 0)
{
return 0;
}
m = m % n;
ListNode* head = new ListNode(0);
ListNode* tmp = head;
// 构建一个环形链表
for(int i = 1; i < n; i++)
{
ListNode* h = new ListNode(i);
tmp->next = h;
tmp = h;
}
tmp->next = head;
/* 验证链表构建是否正确
tmp = head;
for(int i = 0; i < n + 1; i++)
{
cout << tmp->val << " ";
tmp = tmp->next;
}
cout << endl;
*/
if (m == 1) // 一直在删除当前节点,剩余的一定是尾节点
{
return tmp->val;
}
tmp = head;
while(tmp->next != tmp)
{
for(int i = 1; i < m - 1; i++)
{
tmp = tmp->next;
}
// cout << tmp->val << endl;
ListNode* del = tmp->next;
tmp->next = tmp->next->next;
delete del;
tmp = tmp->next;
}
return tmp->val;
}
};
解法二:找规律,(自己推一推)最终归纳出在n个数中删除第m个数的最终剩余元素的关系是
参考链接https://blog.csdn.net/u011500062/article/details/72855826
思路上就是,用f(n,m)代表n个人杀掉第m个人之后的幸存者下标。那个f(n-1,m)就是杀掉一个人之后,当前幸存者的下标移动到的位置。因为下一次计数是从被杀掉的下一个开始,因此就是从m+1开始记。因此幸存者的下标往前移动了m位。反推,则得到f(n,m) = f(n-1,m) + m。因为下标可能存在越界,因此更改为f(n,m)=[f(n-1,m) + m] % n。
过程
class Solution {
public:
int lastRemaining(int n, int m) {
if (n == 0)
{
return 0;
}
if (m == 1) // 总是删除当前节点,最后获得的一定是最后一个节点
{
return n - 1;
}
// 递归写法,效率低
// return (lastRemaining(n-1, m) + m) % n;
int* result = new int[n];
result[0] = 0;
for(int i = 1; i < n; i++)
{
result[i] = (result[i-1] + m) % (i+1);
// cout << i << " " << result[i] << endl;
}
return result[n-1];
}
};