Problem 79 : Passcode derivation
A common security method used for online banking is to ask the user for three random characters from a passcode. For example, if the passcode was 531278, they may ask for the 2nd, 3rd, and 5th characters; the expected reply would be: 317.
The text file, keylog.txt, contains fifty successful login attempts.
Given that the three characters are always asked for in order, analyse the file so as to determine the shortest possible secret passcode of unknown length.
1. 欧拉项目第79道题 : 密码推导
用于网上银行的一种常见的安全方法是向用户询问密码中的三个随机字符。例如,如果密码为531278,他们可能要求第2、第3和第5个字符;预期的答复是:317。
文本文件(keylog.txt ) 包含50次成功的登录尝试。
鉴于这三个字符总是按序被询问,分析文件,以确定最短可能的未知长度的保密的密码。
2. 求解分析
如何推导密码呢? 分为三步,
第一步: 因为询问是按顺序的, 我们先统计每个数字的前面的数字集合和它后面的数字集合, 考虑到第一步都是直接的, 我们还需要进行第二步, 把间接的数字也增加进来,
第二步: 就是遍历每个数字的后面的数字集合, 把有些间接的数字放进集合里。
第三步: 就是排序,如果一个数字后面的数字集合里的项越多,它就排在前面,一个数字后面的数字集合的项越少,它就排在后面,当然如果一个数字前面的数字集合和后面的数字集合的项都为空,那这个数字就没有被使用。
Step1: get info after reading keylog :
The digits behind digit 0 :
The digits behind digit 1 : 0, 2, 6, 8, 9,
The digits behind digit 2 : 0, 8, 9,
The digits behind digit 3 : 1, 6, 8,
The digits behind digit 4 :
The digits behind digit 5 :
The digits behind digit 6 : 0, 2, 8, 9,
The digits behind digit 7 : 1, 2, 3, 6, 9,
The digits behind digit 8 : 0, 9,
The digits behind digit 9 : 0,
Step 2: after derivation and before descending sort :
The digits behind digit 0 :
The digits behind digit 1 : 0, 2, 6, 8, 9,
The digits behind digit 2 : 0, 8, 9,
The digits behind digit 3 : 0, 1, 2, 6, 8, 9,
The digits behind or before digit 4 : none
The digits behind or before digit 5 : none
The digits behind digit 6 : 0, 2, 8, 9,
The digits behind digit 7 : 0, 1, 2, 3, 6, 8, 9,
The digits behind digit 8 : 0, 9,
The digits behind digit 9 : 0,
Step 3: after derivation and after descending sort:
The digits behind digit 7 : 0, 1, 2, 3, 6, 8, 9,
The digits behind digit 3 : 0, 1, 2, 6, 8, 9,
The digits behind digit 1 : 0, 2, 6, 8, 9,
The digits behind digit 6 : 0, 2, 8, 9,
The digits behind digit 2 : 0, 8, 9,
The digits behind digit 8 : 0, 9,
The digits behind digit 9 : 0,
The digits behind or before digit 5 : none
The digits behind or before digit 4 : none
The digits behind digit 0 :
最后,我们就可以推断出密码的所有的数字了。所以,第一步是基础,第二步最重要,不少人就是就是用纸和笔做的第二步,第三步排序也很重要,排序后,密码就水落石出了。
3. C++代码实现
我们根据求解分析的主要三步,定义了类PE0079, 类图如下:
首先,我们定义了一个struct _Digit:
typedef struct _Digit
{
int d; // digit
bool is_used; // this flag indicates whether digit is used
set<int> digits_s; // digits set behind digit
set<int> before_digits_s; // digits set before digit
} Digit;
类PE0079有一个数据成员:vector m_digits_vec,保存从0到9每个数字的信息;
类PE0079还有几个成员函数,其中第一步在函数readKeylog()里实现,第二步和第三步在函数findShortestSecretPasscode()里实现。
C++代码
#include <iostream>
#include <vector>
#include <set>
#include <iterator>
#include <fstream>
#include <sstream>
#include <algorithm>
using namespace std;
#define UNIT_TEST
typedef struct _Digit
{
int d; // digit
bool is_used; // this flag indicates whether digit is used
set<int> digits_s; // digits set behind digit
set<int> before_digits_s; // digits set before digit
} Digit;
class PE0079
{
private:
vector<Digit> m_digits_vec;
int readKeylog(char *filename);
void printDigitsDebugInfo();
void printDigitsInfo();
public:
PE0079();
int findShortestSecretPasscode();
};
bool myless(const Digit& digit1, const Digit& digit2);
PE0079::PE0079()
{
Digit digit;
for (int i = 0; i < 10; i++)
{
digit.d = i;
digit.is_used = true;
m_digits_vec.push_back(digit);
}
}
bool myless(const Digit& digit1, const Digit& digit2)
{
int size1 = digit1.digits_s.size();
int size2 = digit2.digits_s.size();
if (size1 > size2)
{
return true;
}
else if (size1 == size2)
{
return digit1.d > digit2.d;
}
return false;
}
void PE0079::printDigitsDebugInfo()
{
for (int i = 0; i < 10; i++)
{
int k = m_digits_vec[i].d;
if (true == m_digits_vec[i].is_used)
{
cout << "The digits behind digit " << k << " : ";
set<int> tmp_digits_s = m_digits_vec[i].digits_s;
copy(tmp_digits_s.begin(), tmp_digits_s.end(), \
ostream_iterator<int>(cout, ", "));
cout << endl;
}
else
{
cout << "The digits behind or before digit "<<k<<" : none"<<endl;
}
}
}
void PE0079::printDigitsInfo()
{
for (int i = 0; i < 10; i++)
{
if (true == m_digits_vec[i].is_used)
{
cout << m_digits_vec[i].d;
}
}
cout << endl;
}
int PE0079::readKeylog(char *filename)
{
char szBuf[8];
ifstream in(filename);
if (!in) return -1;
int number;
int digit, last_digit;
while (in.getline(szBuf, 8))
{
string strText = szBuf;
istringstream s(strText);
s >> number;
last_digit = -1;
while (number > 0)
{
digit = number % 10;
number /= 10;
if (-1 != last_digit)
{
m_digits_vec[digit].digits_s.insert(last_digit);
m_digits_vec[last_digit].before_digits_s.insert(digit);
}
last_digit = digit;
}
}
return 0;
}
int PE0079::findShortestSecretPasscode()
{
char filename[] = "p079_keylog.txt";
if (0 != readKeylog(filename))
{
return -1;
}
#ifdef UNIT_TEST
cout << "Step 1: get info after reading keylog : " << endl;
printDigitsDebugInfo();
#endif
for (int i = 0; i < 10; i++)
{
if (0 != m_digits_vec[i].digits_s.size() ||
0 != m_digits_vec[i].before_digits_s.size())
{
for (auto digit : m_digits_vec[i].digits_s)
{
for (auto digit_new : m_digits_vec[digit].digits_s)
{
m_digits_vec[i].digits_s.insert(digit_new);
}
}
}
else
{
m_digits_vec[i].is_used = false;
}
}
#ifdef UNIT_TEST
cout << "Step 2: after derivation and before descending sort : "<<endl;
printDigitsDebugInfo();
#endif
sort(m_digits_vec.begin(), m_digits_vec.end(), myless);
#ifdef UNIT_TEST
cout << "Step 3: after derivation and after descending sort: " << endl;
printDigitsDebugInfo();
#endif
cout << "The shortest possible secret passcode of unknown length is ";
printDigitsInfo();
return 0;
}
int main()
{
PE0079 pe0079;
pe0079.findShortestSecretPasscode();
return 0;
}
4. Python代码实现
Python也采用了求解分析的方法,但是由于数据结构的原因,Python和C++稍微有点不同。
为了控制打印Debug 信息,一个全局的变量 PE0079_Eanble_Debug 被定义:
PE0079_Eanble_Debug = True, 打印出所有的Debug信息,否则,PE0079_Eanble_Debug = False, 不会打印任何Debug信息。
Python 代码
class PE0079(object):
def __init__(self):
self.m_digits_list = []
for i in range(10):
# digit tuple = (digit, digits set behind digit, digits set before digit)
digit_tuple = i, set(), set()
self.m_digits_list += [ digit_tuple ]
def printStep1DebugInfo(self):
for digit_tuple in self.m_digits_list:
print('The digits behind digit %d :' % digit_tuple[0], digit_tuple[1])
print('The digits before digit %d :' % digit_tuple[0], digit_tuple[2])
def printStep2DebugInfo(self, tmp_passcode_list):
print("Step 2: after derivation and before descending sort : ")
for index, digits_list in enumerate(tmp_passcode_list):
if len(digits_list) != 0 or len(self.m_digits_list[index][2]) != 0:
print('The digits behind digit %d :'% index, digits_list)
else:
print('The digits behind or before digit %d : none'% index)
def printStep3DebugInfo(self, final_passcode_list):
print("Step 3: after derivation and after descending sort: ")
for digit_tuple in final_passcode_list:
print('The digits behind digit %d :'%digit_tuple[0], digit_tuple[1])
def printDigitsInfo(self, passcode_list):
for digit_tuple in passcode_list:
print(digit_tuple[0], end='')
print()
def readKeylog(self, filename):
for line in open(filename).readlines():
number, last_digit = int(line), -1 # one number per line
while number > 0:
digit, number = number % 10, number // 10
if -1 != last_digit:
digit_tuple = self.m_digits_list[digit]
self.m_digits_list[digit][1].add(last_digit)
last_digit_tuple = self.m_digits_list[last_digit]
last_digit_tuple[2].add(digit)
last_digit = digit
def findShortestSecretPasscode(self):
self.readKeylog('p079_keylog.txt')
if True == PE0079_Eanble_Debug:
print("Step 1: get info after reading keylog :")
self.printStep1DebugInfo();
tmp_passcode_list = []
for digit_tuple in self.m_digits_list:
tmp_list = list(digit_tuple[1]) # digits list behind digit
if 0 != len(digit_tuple[1]) or 0 != len(digit_tuple[2]):
if len(digit_tuple[1]) > 0:
for digit in digit_tuple[1]:
digit_new_tuple = self.m_digits_list[digit]
for digit_new in digit_new_tuple[1]:
if digit_new not in tmp_list:
tmp_list += [ digit_new ]
tmp_passcode_list += [ tmp_list]
if True == PE0079_Eanble_Debug:
self.printStep2DebugInfo(tmp_passcode_list)
final_passcode_list = []
for index, list_behind_digit in enumerate(tmp_passcode_list):
if len(list_behind_digit) != 0 or len(self.m_digits_list[index][2]) != 0:
final_passcode_list += [(index, list_behind_digit)]
final_passcode_list.sort(key=lambda passcode: len(passcode[1]), reverse=True)
if True == PE0079_Eanble_Debug:
self.printStep3DebugInfo(final_passcode_list)
print("The shortest possible secret passcode of unknown length is ", end='');
self.printDigitsInfo(final_passcode_list)
return 0
def main():
global PE0079_Eanble_Debug
PE0079_Eanble_Debug = True
pe0079 = PE0079()
pe0079.findShortestSecretPasscode()
if __name__ == '__main__':
main()
Python代码 II (通过使用列表的deepcopy,最终解决了RuntimeError: Set changed size during iteration)
import copy
class PE0079(object):
def __init__(self):
self.m_digits_list = []
for i in range(10):
# d_tuple = (digit, digits set behind digit, digits set before digit)
self.m_digits_list += [ (i, set(), set()) ]
def printDebugInfo(self):
for d_tuple in self.m_digits_list:
if len(d_tuple[1]) != 0 or len(d_tuple[2]) != 0:
print('The digits behind digit %d :' %d_tuple[0],d_tuple[1])
#print('The digits before digit %d :' %d_tuple[0],d_tuple[2])
else:
print('The digits behind or before digit %d : none'%d_tuple[0])
print('')
def printDigitsInfo(self):
for digit_tuple in self.m_digits_list:
if len(digit_tuple[1]) != 0 or len(digit_tuple[2]) != 0:
print(digit_tuple[0], end='')
print('')
def readKeylog(self, filename):
for line in open(filename).readlines():
number, last_digit = int(line), -1 # one number per line
while number > 0:
digit, number = number % 10, number // 10
if -1 != last_digit:
self.m_digits_list[digit][1].add(last_digit)
self.m_digits_list[last_digit][2].add(digit)
last_digit = digit
def findShortestSecretPasscode(self):
self.readKeylog('p079_keylog.txt')
if True == PE0079_Eanble_Debug:
print("Step 1: get info after reading keylog :")
self.printDebugInfo();
# for digit in digit_tuple[1]:
# RuntimeError: Set changed size during iteration
tmp_passcode_digits_list = copy.deepcopy(self.m_digits_list) # required!!
for d, digit_tuple in enumerate(tmp_passcode_digits_list):
if len(digit_tuple[1]) > 0:
for digit in digit_tuple[1]:
behind_digit_tuple = tmp_passcode_digits_list[digit]
for behind_digit in behind_digit_tuple[1]:
if behind_digit not in digit_tuple[1]:
self.m_digits_list[d][1].add(behind_digit)
#if len(digit_tuple[2]) > 0:
# for digit in digit_tuple[2]:
# before_digit_tuple = tmp_passcode_digits_list[digit]
# for before_digit in before_digit_tuple[2]:
# if before_digit not in digit_tuple[2]:
# self.m_digits_list[d][2].add(before_digit)
if True == PE0079_Eanble_Debug:
print("Step 2: after derivation and before descending sort : ")
self.printDebugInfo()
# descending sort, by the number of digits behind digit
self.m_digits_list.sort(key=lambda d_tuple: len(d_tuple[1]), reverse=True)
if True == PE0079_Eanble_Debug:
print("Step 3: after derivation and after descending sort: ")
self.printDebugInfo()
print("The shortest possible secret passcode of unknown length is ",end='')
self.printDigitsInfo()
return 0
def main():
global PE0079_Eanble_Debug
PE0079_Eanble_Debug = True
pe0079 = PE0079()
pe0079.findShortestSecretPasscode()
if __name__ == '__main__':
main()