Categories
不学无术

LeetCode 43. Multiply Strings

注意/思路

  • 保存 被乘数*[0…9]的结果备用,然后移位相加即可
  • 乘数末尾是0

代码

9ms

class Solution {
public:
    void ReverseStr(string& str){
        int left=0, right = str.size()-1;
        while(left<right){
            char c = str[left];
            str[left] = str[right];
            str[right] = c;
            left++;
            right--;
        }
    }
    void AddResult(string& result, const string& subProduct, const int shift){
        int carry = 0;
        while(result.size() < shift){
            result += "0";
        }
        for(int i=shift; i<subProduct.size() + shift; i++){
            if(i < result.size()){
                int subSum = (result[i]-'0') + (subProduct[i-shift]-'0') + carry;
                if(subSum >= 10){
                    subSum-= 10;
                    carry = 1;
                } else {
                    carry = 0;
                }
                result[i] = subSum + '0';
            } else {
                int subSum = (subProduct[i-shift]-'0') + carry;
                carry = subSum / 10;
                subSum = subSum % 10;
                result = result + (char)(subSum+'0');
            }
        }
        if (carry > 0) {
			result += (char)(carry + '0');
		}
    }
    string getSubProduct(char digit, string op){
        int carry = 0;
        int d = digit-'0';
        for(int i=0; i<op.size(); i++){
            int prod = (op[i]-'0') * d + carry;
            carry = prod / 10;
            prod = prod % 10;
            op[i] = '0' + prod;
        }
        if(carry > 0){
            op = op + (char)(carry+'0');
        }
        return op;
    }
    string multiply(string op1, string op2) {
        string result;
        if(op1.empty() || op2.empty())
            return result;
        if(op1.size() < op2.size()){
            string temp = op1;
            op1 = op2;
            op2 = temp;
        }
        string productSub[10];
        ReverseStr(op1);
        ReverseStr(op2);
        for(int i=0; i<op2.size(); i++){
            char digit = op2[i];
            if(digit == '0')
                continue;
            if(productSub[digit-'0'].empty()){
                productSub[digit-'0'] = getSubProduct(digit, op1);
            }
            AddResult(result, productSub[digit-'0'], i);
        }
        if(result.empty())
            return "0";
        ReverseStr(result);
        return result;
    }
};

 

Categories
不学无术

LeetCode 103. Binary Tree Zigzag Level Order Traversal

Given a binary tree, return the zigzag level order traversal of its nodes’ values. (ie, from left to right, then right to left for the next level and alternate between).
For example:
Given binary tree [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

return its zigzag level order traversal as:

[
  [3],
  [20,9],
  [15,7]
]

思路

记住上一行的输出(结果)节点,每一次从后往前读出下一行,这样第2,4,6次读的时候其实就会翻转成正向了。唯一的问题是左右子节点写入下一行的顺序,按照当前顺序区分一下就行。不是左右左右,就是右左右左,具体看code吧。

代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> result;
        if(root == NULL)
            return result;
        vector<TreeNode*>* currRow = new vector<TreeNode*>();
        vector<TreeNode*>* nextRow = new vector<TreeNode*>();
        bool direction = true;  //true: R,L,R,L,...; false:L,R,L,R,...
        currRow->push_back(root);
        while(!currRow->empty()){
            //Output current row
            vector<int> currRowResult;
            for(int i=0; i<currRow->size(); i++){
                currRowResult.push_back( (*currRow)[i]->val );
            }
            result.push_back(currRowResult);
            //
            for(int i=currRow->size()-1; i>=0; i--){
                TreeNode* currNode = (*currRow)[i];
                if(direction){
                    if(currNode->right != NULL)
                        nextRow->push_back(currNode->right);
                    if(currNode->left != NULL)
                        nextRow->push_back(currNode->left);
                } else {
                    if(currNode->left != NULL)
                        nextRow->push_back(currNode->left);
                    if(currNode->right != NULL)
                        nextRow->push_back(currNode->right);
                }
            }
            //swap currRow and nextRow
            vector<TreeNode*>* temp = currRow;
            currRow = nextRow;
            nextRow = temp;
            nextRow->clear();
            direction = !direction;
        }
        return result;
    }
};

 

Categories
不学无术

LeetCode 50. Pow(x, n)

边界检查:0,INT_MAX, INT_MIN

class Solution {
private:
    map<int, double> memory;
public:
    double myPow(double x, int n) {
        if(n == 0)
            return 1.0;
        if(n == 1)
            return x;
        if(n == -1)
            return 1/x;
        if(memory.count(n)>0)
            return memory[n];
        double result = myPow(x, n/2) * myPow(x, n/2);
        if(n%2 != 0){
            result *= (n<0)? 1/x : x;
        }
        memory[n] = result;
        return result;
    }
};

 

Categories
不学无术

LeetCode 134. Gas Station

思路

O(n^2)做法:直接嵌套循环,暴力试就行
O(n)做法:类似贪心的思路。主要还是看gas[i]-cost[i],如果减出来是正的,那么可以为后面的路段留下点资源,如果减出来是负的话,那么最好的做法是在之前积累最多的资源来解决这个debt. 那什么时候有可能积累到最多资源呢?只能将这个有欠债的站点作为最后一站,所以将该站点的下一站作为起始试试。

代码

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int totalLoss = 0;
        int tank = 0;
        int max_i = 0;
        for(int i=0; i<gas.size(); i++){
            tank += gas[i] - cost[i];
            if(tank < 0){   //Owe!
                max_i = i+1; //next possible start
                totalLoss += tank;  //add in the debt
                tank = 0;
            }
        }
        if(totalLoss + tank < 0)    //if the debt cannot be paid
            return -1;
        return max_i;
    }
};

 

Categories
不学无术

LeetCode 7. Reverse Integer

Reverse digits of an integer.
Example1: x = 123, return 321
Example2: x = -123, return -321

思路

检测溢出要在溢出之前,因为溢出的结果是undefined的。
比如要判断加法溢出,想当然的就是 A + B > INT_MAX,但是这是不对的,A + B指不定加出什么东西来,应当是 A > INT_MAX – B.

代码

class Solution {
public:
    bool safeMultiply10(int& result){
        //Multiply result by 10 if not overflow
        //Return false if it overflows
        if(result > 0){
            if(result > INT_MAX / 10)
                return false;
        } else if(result < 0) {
            if(result < INT_MIN / 10)
                return false;
        }
        result *= 10;
        return true;
    }
    bool safePlus(int& result, int r){
        //Return false if it overflows
        if(result > 0){
            if(result > INT_MAX - r)
                return false;
        } else if (result < 0) {
            if(result < INT_MIN - r)
                return false;
        }
        result += r;
        return true;
    }
    int reverse(int x) {
        int result = 0;
        //bool negFlag = x<0;
        while(x != 0){
            int r = x % 10;
            if(!safeMultiply10(result))
                return 0;
            if(!safePlus(result, r))
                return 0; //overflow
            x /= 10;
        }
        return result;
    }
};

 

Categories
不学无术

除法的效率

估计应该都知道算除法的效率比算+-*低好多,那么到底低多少呢?简单写了一个程序测试看看。
注意:avg0函数可能会碰到溢出的情况,这种方法尽量不要用。

  • avg0: 统一加起来,再做除法;
  • avg1: 加的时候就做除法;
  • avg2: 加的时候乘以预先算好的,分母的倒数;
void printDuration(clock_t start, clock_t end){
    cout << "Time elapsed: " << (end - start)/(float)CLOCKS_PER_SEC << " seconds." << endl;
}
double avg0(int* ary, int N, long again){
    cout << "avg0: ";
    clock_t start = clock();
    double sum;
    while(again > 0){
        sum = 0;
        for(int i=0; i<N; i++)
            sum += ary[i];
        sum /= (double)N;
        again --;
    }
    clock_t end = clock();
    printDuration(start, end);
    return sum;
}
double avg1(int* ary, int N, long again){
    cout << "avg1: ";
    clock_t start = clock();
    double sum = 0;
    while(again > 0){
        for(int i=0; i<N; i++)
            sum += ary[i]/(double)N;
        again--;
    }
    clock_t end = clock();
    printDuration(start, end);
    return sum;
}
double avg2(int* ary, int N, long again){
    cout << "avg2: ";
    clock_t start = clock();
    double sum = 0;
    while(again > 0){
        double denominator = 1 / (double)N;
        for(int i=0; i<N; i++)
            sum += ary[i] * denominator;
        again--;
    }
    clock_t end = clock();
    printDuration(start, end);
    return sum;
}
int main() {
    int N;
    long again;
    cin >> N >> again; //num of numbers to be generated
    srand(time(NULL));
    int* ary = new int[N];
    for(int i=0; i<N; i++)
        ary[i] = rand();
    double r0 = avg0(ary, N, again);
    double r1 = avg1(ary, N, again);
    double r2 = avg2(ary, N, again);
    return 0;
}

结果:

50000 50000
avg0: Time elapsed: 7.49197 seconds.
avg1: Time elapsed: 16.7689 seconds.
avg2: Time elapsed: 7.51349 seconds.
Process finished with exit code 0

比一倍还多的时间!可怕!

Categories
不学无术

用于Sentence Embedding的DSSM与LSTM:管中窥豹

前置废话:感觉实习这两个月真是顶得上实验室半年,想想对不起我的导师,跟现在相比之前天天像是在打酱油啊。

相关文献

  1. Huang, Po-Sen, et al. “Learning deep structured semantic models for web search using clickthrough data.” Proceedings of the 22nd ACM international conference on Conference on information & knowledge management. ACM, 2013.
  2. Palangi, Hamid, et al. “Deep sentence embedding using long short-term memory networks: Analysis and application to information retrieval.” IEEE/ACM Transactions on Audio, Speech, and Language Processing 24.4 (2016): 694-707.

DSSM一撇

DSSM了解的不是很多,但是这两篇文章有几处是类似的,隐隐地感觉后面这篇是借鉴前面的写出来的。
文章中DSSM的最终目标是实现一个神经网络模型,使得给定输入文档(片段)可以输出一个对应的向量,而通过向量之间的余弦相似度可以判断两个Query-Document的相似性[latex]R(Q,D)=cosine(y_Q,y_D)[/latex],然后拿这个就能做搜索排序了。

Word-Hashing

DSSM中用到了一个奇淫巧术,就是所谓的Word Hashing方法。我们知道,要将文字拿来处理,首先要表示成向量的形式(数值化),最普通的方法是做One-Hot Vector, 即每个单词对应一个feature值,出现就是1,没出现就是0. 但是单词表实在是太大了,这篇论文用到了一个叫Letter-N-Gram的技术,即将一个单词强行分成几个n-gram. 例如单词Hello做Letter-3-Gram,就会变成#HE, HEL, ELL, LLO, LO# 这样的表示方法(其中#代表开头或结尾)。由于处理的Query/Doc对中特殊字符会在数据预处理阶段被过滤,因此抛开单词大小写,a-z0-9#一共就26+10+1=37个字符,37*37*37=50653种可能性,相比词典来说小多了!
这个办法初看不是很靠谱,但是实验结果却是不错的。

正例/反例

训练神经网络需要给正例以及反例。不出意外地,这里用的是用户的click stream,即用户的点击数据。假设[latex]D[/latex]表示对于一个查询Q的所有候选文档,那么[latex]D^+[/latex]表示用户点击的文档作为正例,[latex]D^-[/latex]表示没有点击的作为负例。再根据上面提到的相似度[latex]R(Q,D)[/latex],可以做出一个softmax的后验概率:

[latex]P(D|Q)=\frac{exp(\gamma R(Q,D))}{\sum_{D’ \in D}exp(\gamma R(Q,D’))}[/latex]

通俗易懂,对吧。

神经网络结构

DSSM
想要实战的,可以看看CNTK里面有一个类似的例子:网络定义文件在这里,不过这个文件里面已经是Word Hashing过之后的了,而且参数设置和论文里的不大一样。
上面Figure1可以根据每一列来看。输入的最底层是Query/Doc的高纬度的词向量(Term Vector),然后进入了Word Hashing阶段把高维词向量弄成了L3G之类的向量,维度大概在30k~50k之间,然后加了两个隐藏层后输出了最终的128维向量y。这个向量y在训练结束之后就是传说中的词向量了。然后两个词向量之间可以算出相似度,最终给出一个softmax算的后验概率[latex]P(D_i|Q)[/latex]。
训练整一坨神经网络要给一个优化目标,文中定义了一个loss function:

[latex]L(\Lambda)=-log\prod_{(Q,D^+)}P(D^+|Q)[/latex]

其中[latex]\Lambda[/latex]表示训练神经网络时的一堆参数。然后使用梯度下降之类的方法,把梯度一层层往回传就能调整参数了。

RNN与LSTM

RNN/LSTM具体是怎么工作的?要详细理解可以看这篇文章,讲得很透彻。这里只提到论文中的一些实现。

RNN

 
RNN Sentence Embedding
RNN的结构特点是下一个平行节点状态的信息是基于之前节点的。如上图,在本文的环境中,底下的x0,x1,x2,…可以看做一个个单词的输入,这一长串的节点连在一起就变成了一个句子。上面做DSSM时,一整句(段)话的词都拿来一起做Word Hashing,结果就是词汇之间的信息从一开始就会被忽略掉,如果换成了RNN,虽然最后拿到的还是个句子的向量,但是理论上训练出的模型会更加准确一些。中间层是Word Hashing之后的层,用的应该是和DSSM一样的Hashing方法。在最右侧(最后一个词)的激活输出就是Embedding好的句子,一个句子向量:)
但是RNN的引入会引出“梯度爆炸”或“梯度消失”的问题。数学原理不多说,考虑这个句子:I grew up in France… I speak fluent French. 假设我的任务是要训练一个RNN,根据之前的所有单词信息“猜”最后一个词汇French。在这句话中,显然最适合用来参考的是句子开头的那个词France,但是中间还隔了一大堆的“没卵用”词。在根据标签判断loss函数做梯度下降的时候,梯度一层层往回传,但由于中间隔的比较远,等轮到France这个词的时候已经几乎没有影响了。

LSTM-DSSM

Sentence Embedding: LSTM - DSSM
LSTM处于什么位置呢?我把两个文章的对比图放在这边,基本上就能看出来了。左图里面用橙色圈出的结构,他的作用和右侧黑色虚线框里的东西是一样的,只是LSTM结构取代了原本比较“单纯”的多层网络的地位。所以这个论文的方法似乎可以叫做LSTM-DSSM.
所以对于怎么做学习,这个新文章的方法也是与它的前身很类似的:根据用户点击的Query-Doc来确定正例及反例(右图中的[latex]D^+[/latex]与[latex]D^-[/latex]),求一个最大似然,它等同于最小化下面这个公式中的参数[latex]\Lambda[/latex]。

[latex]L(\Lambda)=-log\prod_{(Q,D^+)}P(D^+|Q)=\sum_{r=1}^{n}l_r(\Lambda)[/latex]

ML of DSSM-LSTM
其中,r对应第r条Query, j是这条Query对应的一条Document。
多说一句,这个Query-Doc的匹配过程有点像做Translation Model,只不过Translation Model有Encoding和Decoding两步,而这里只有Encoding的过程。

LSTM结构

简单来说,LSTM是一种RNN结构,里面通过一些Gate来实现“要不要记住/考虑XXX”的选择。LSTM根据需要有不同的具体实现,下图是论文中用到的结构:
LSTM Cell
这个结构分成了几个关键部位:Input Gate, Output Gate 和 Forget Gate. 可以看到这些Gate的实现是Sigmoid输出的(图中[latex]\sigma(.)[/latex]),所以值都在0-1之间;然后和前面的h(.)去做一个pointwise multiple(X),实现了“门”的功能。

  • Input Gate决定了在这个LSTM结构中哪些值是要更新的,即是否要把当前的输入信息加入到隐藏层的状态;
  • Output Gate决定了是否把当前这个LSTM的输出传到下一层去;
  • Forget Gate决定了是否要保留当前一个节点的历史状态c(t-1),可以看到图中有一个圈状的结构

 

Categories
不学无术

LeetCode 146. LRU Cache

思路

双向链表+哈希表实现,要明白STL里的<map>是红黑树实现的,亲测速度不够,要用真.哈希表:<unordered_map>
可能有人要问,为啥不用单向链表?我的理解是为了删除元素的时候不用从头搜索。
当然,针对这道题是可以用单向链表的,即“删除”最无用节点的时候,对链表不作处理,而只是从哈希表里把值删掉。反正在搜索的时候也是直接查的哈希表。副作用是这个链表会越来越长。
另外,不想自己实现双向链表的话,可以用<list>

代码

struct LinkedNode{
    int key;
    LinkedNode* next;
    LinkedNode* prev;
    LinkedNode(int _key = -1){
        key = _key;
        next = NULL;
        prev = NULL;
    }
};
class LRUCache{
private:
    LinkedNode* pHeader;
    LinkedNode* pEnd;
    unordered_map<int, int> keyValueMap;
    int cap;
public:
    LRUCache(int capacity) {
        cap = capacity;
        pHeader = new LinkedNode(); //dummy node
        pEnd = new LinkedNode();
        pHeader->next = pEnd;
        pEnd->prev = pHeader;
    }
    void refreshQueue(int key){
        LinkedNode* pNode = pHeader;
        while(pNode->next != NULL){
            if(pNode->next->key == key)
                break;
            pNode = pNode->next;
        }
        //Move pNode->next to head
        LinkedNode* pPrevNode = pNode;
        LinkedNode* pHitNode = pNode->next;
        LinkedNode* pNextNode = pHitNode->next;
        pPrevNode->next = pNextNode;
        pNextNode->prev = pPrevNode;
        pHeader->next->prev = pHitNode;
        pHitNode->prev = pHeader;
        pHitNode->next = pHeader->next;
        pHeader->next = pHitNode;
    }
    int removeLast(){
        //Remove the last item and return its key
        LinkedNode* pToRemove = pEnd->prev;
        int key = pToRemove->key;
        LinkedNode* pPrev = pToRemove->prev;
        pPrev->next = pEnd;
        pEnd->prev = pPrev;
        delete pToRemove;
        return key;
    }
    int get(int key) {
        if(keyValueMap.count(key) > 0){
            //Change priority queue
            refreshQueue(key);
            //Get value
            return keyValueMap[key];
        } else {
            return -1;
        }
    }
    void insert(int key){
        LinkedNode* pNext = pHeader->next;
        LinkedNode* pNewNode = new LinkedNode(key);
        pNewNode->next = pNext;
        pNewNode->prev = pHeader;
        pHeader->next = pNewNode;
        pNext->prev = pNewNode;
    }
    void set(int key, int value) {
        if(keyValueMap.count(key)){
            keyValueMap[key] = value; //set key
            //Refresh queue
            refreshQueue(key);
        } else {
            if(keyValueMap.size() == cap){
                int oldKey = removeLast();
                keyValueMap.erase(oldKey);
            }
            keyValueMap[key] = value;
            insert(key);
        }
    }
};

 

Categories
不学无术

LeetCode 139. Word Break

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
For example, given
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".

思路

字符串处理的一个题目,首先想到的办法就是穷举:从头开始找N-letter word,查一下在不在字典里呗。但是纯穷举的话显然就会超时,因为这个问题可以分解成下面的子问题,即给定字符串s及起始位置start,寻找s[start…end]能不能够被拆分。这个子问题在暴力搜索的时候很容易被重复,所以得找个东西记录一下状态,碰到相同的子问题就不用再去查字典了。
所以,他是个动态规划的题。

代码

bool wordBreakSub(const string& s, unordered_set<string>& wordDict, int startPos, int* memory)
{
    if(memory[startPos] != -1)
        return memory[startPos]!= 0;
    string subStr;
    bool canBreak = false;
    for(int len=1; len <= s.size() - startPos; len++)
    {
        subStr = s.substr(startPos, len);
        if(wordDict.count(subStr) > 0) {
            if (startPos + len == s.size()) {
                memory[startPos] = 1;
                return true;
            }
            canBreak = wordBreakSub(s, wordDict, startPos + len, memory);
        }
        if(canBreak)
            break;
    }
    memory[startPos] = canBreak ? 1 : 0;
    return canBreak;
}
bool wordBreak(string s, unordered_set<string>& wordDict)
{
    int* memory = new int[s.size()];
    for(int i=0; i<s.size(); i++)
        memory[i] = -1; //-1:untest; 0-false; 1-true
    return wordBreakSub(s, wordDict, 0, memory);
}

 

Categories
不学无术

LeetCode 116. Populating Next Right Pointers in Each Node

思路

发现是一个树的遍历的题目,而且题目限制的比较死,是个满二叉树,这样处理的时候就避免了比较烦人的跨树指针的情况。比如下面这种建立4->5的指针:

    1
  2   3
4    5  6

第一时间想到的解法是弄个工作队列,然后一层一层遍历下去就行了,这个解法的空间复杂度是O(n),不符合题目要求。后来再一想,其实既然上一层已经把下一层的next指针建立好了,就不用再从工作队列里去“回忆”了,只要在每一行的开头记住下一行在哪里就行,这样只要O(1)空间复杂度。
另外是如何设置next指针,一共有两个情况:

  1. node自己的左孩子和右孩子连起来
  2. node的右孩子与node右侧节点它的左孩子连起来

代码

O(n)空间复杂度方法

/**
 * Definition for binary tree with next pointer.
 * struct TreeLinkNode {
 *  int val;
 *  TreeLinkNode *left, *right, *next;
 *  TreeLinkNode(int x) : val(x), left(NULL), right(NULL), next(NULL) {}
 * };
 */
class Solution {
public:
    void connect(TreeLinkNode *root) {
        queue<TreeLinkNode*> workingQ;
        if(root == NULL)
            return;
        workingQ.push(root);
        while(!workingQ.empty())
        {
            TreeLinkNode* node = workingQ.front();
            workingQ.pop();
            if(node->left == NULL || node->right == NULL)
                continue;   //Leaf node
            node->left->next = node->right; //Inner connection
            if(node->next != NULL)
                node->right->next = node->next->left; //Outter connection
            workingQ.push(node->left);
            workingQ.push(node->right);
        }
    }
};

O(1)空间复杂度方法

class Solution {
public:
    void connect(TreeLinkNode *root) {
        TreeLinkNode* nextRowPtr = root;
        TreeLinkNode* node = root;
        bool atRowStart = true;
        while(nextRowPtr != NULL)
        {
            if(node->left == NULL || node->right == NULL)
                break;   //Leaf node
            if(atRowStart)
            {
                atRowStart = false;
                nextRowPtr = node->left;
            }
            node->left->next = node->right; //Inner connection
            if(node->next != NULL)
            {
                node->right->next = node->next->left; //Outter connection
                node = node->next;
            }
            else
            {
                node = nextRowPtr;
                atRowStart = true;  //End of current row
            }
        }
    }
};