• 面试算法题


    1. 给一颗多叉树,求 从一个节点出发到其它所有节点的距离之和 的最小值。

    树形 dp。一般两遍 dfs 就能解决。
    第一遍 dfs 用 son[i] 记录每个节点多少个子孙,用 dis[i] 记录 i 点到其所有子孙的距离之和。 son[i]和 dis[i]都在回溯的过程进行维护。假设 v 是 u 的孩子节点,(son[u]+=son[v]+1), (dis[u] += dis[v]+son[v]+1),也就是说 v 的每个子孙到 u 的距离是他们到 v 的距离+1,然后再加上 v 到 u 的距离1。
    第二遍 dfs,维护 dis[i] 为到所有点的距离之和。节点 v 到其它所有节点的距离之和可以用 u 到其它所有节点的距离之和计算出来。因为v和v 的子孙到 v 的距离要比到 u 的距离少1,就减去了son[v]+1,然后剩下 n-son[v]-1个点到 v 的距离要比到 u 的距离多1,就加上了 n-son[v]-1,所以就是 (dis[u]+n-2 imes son[v]-2)
    手写代码,大概是下面这样。

    #include <bits/stdc++.h>
    using namespace std;
    const int N=201000;
    struct edge{
    	int to,next;	
    }e[N];
    int head[N],cnt;
    void add_edge(int u,int v){
    	e[++cnt]=(edge){v,head[u]};
    	head[u]=cnt;
    }
    int son[N],dis[N];
    void dfs1(int u,int fa){
    	for(int i=head[u];i;i=e[i].next){
    		int v = e[i].to;
    		if(v == fa)continue;
    		dfs1(v, u);
    		son[u] += son[v]+1;
    		dis[u] += dis[v]+son[v]+1;
    	}
    }
    int ans=1000000009;
    int n,m;
    void dfs2(int u,int fa){
    	ans=min(ans,dis[u]);
    	for(int i=head[u];i;i=e[i].next){
    		int v = e[i].to;
    		if(v == fa)continue;
    		dis[v]=dis[u]+n-2-2*son[v];
    		dfs2(v, u);
    	}
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	for(int i=0;i<m;i++){
    		int u,v;
    		scanf("%d%d",&u,&v);
    		add_edge(u, v);
    		add_edge(v, u);
    	}
    	dfs1(1, 0);
    	dfs2(1, 0);
    	printf("%d",ans);
    	return 0;
    }
    

    2. 现有 n 个木条,第 i 个长度为 a[i],切割这 n 个木条得到 k 个长度为 L 的木条,L最长是多少?

    二分 L,每个木条就能得到 n/L 个,计算总共能否得到至少 k 个木条。复杂度是O(n)。

    3. k个有序数组怎么归并

    维护一个大小为 k 的小根堆。先把每个数组第一个放入小根堆。每次把最小的取出来放入答案,并且把它所属数组的下一个放入小根堆。

    const int N=205;
    
    vector <int> mergeKArray(int k, vector<int> arr[N]){
    	vector<int> ans;
    	int now[N];
    	priority_queue<pair<int, int>, vector<pair<int,int> >, greater<pair<int,int> > > heap;
    	for(int i=0;i<k;i++){
    		heap.push(make_pair(arr[i][0], i));
    	}
    	while(heap.size()){
    		pair<int,int> u=heap.top(); heap.pop();
    		ans.push_back(u.first);
    		int i=u.second;
    		now[i]++;
    		if(now[i]<arr[i].size()){
    			heap.push(make_pair(arr[i][now[i]],i));
    		}
    	}
    	return ans;
    }
    

    4. 求长度为 2n 的字典序第 k 大的合法括号序列,合法是符合下面两个要求:

    1)空串
    2)若 s 合法,()s、(s)也合法
    思路是长度为2i的合法序列有(2^{i-1})个,所以可以递归构造。如果(2^{n-2} > k),说明一定是()开头,再去构造长度为2(n-1)的第 k 大序列。否则,一定是(s)构成,再去构造长度为2(n-1)的第 (k-2^{n-2})大的序列。

    5. 平面有 n 个 x 坐标不同的点。求斜率最大的两个点。

    按 x 坐标排序,然后斜率最大的两个点一定是相邻的,所以再两个两个判断一遍即可。

    6. 有序数组找出出现超过一半的数。如果不知道有没有超过一半的,怎么判断。如果是判断超过1/3的呢?

    中位数。用中位数的 lowerbound 和 upperbound 判断。用1/3和2/3位置的数的上下界判断。

    7. 给定某单link链表,输出这个链表中倒数第k个结点。链表的倒数第0个结点为链表

    的尾指针。链表结点定义如下:

    struct LinkNode {  int m_nKey;  LinkNode* m_pNext;  };
    
    LinkNode* kthNode(LinkNode* head, int k){
      LinkNode* p = head;
      for(int i = 0; i < k; i++){
        if (p->m_pNext == nullptr) {
          return nullptr; // 不存在倒数第 k 个
        }
        p = p->m_pNext;
      }
    
      LinkNode* ans = head;
    
      for(; p->m_pNext != nullptr; p = p->m_pNext) {
        ans = ans->m_pNext;
      }
      return ans;
    }
    

    8. 给定一个二叉树中的两个节点,返回它们的最近公共祖先节点。

    struct Node {
      int key;
      Node* Fa;
      Node* Lson, *Rson;
    };
    
    Node* LeastCommonAncestors(Node* p, Node* q){
    
      int pDep = 0, qDep = 0;
    
      for(Node* now = p; now->Fa != nullptr; now = now->Fa) {
        pDep++;
      }
      for(Node* now = q; now->Fa != nullptr; now = now->Fa) {
        qDep++;
      }
      Node* nowp = p, *nowq = q;
      for(; nowp != nowq; ) {
        if(pDep > qDep) {
          pDep--;
          nowp = nowp->Fa;
        }
        else if(pDep < qDep){
          qDep--;
          nowq = nowq->Fa;
        }
        else(pDep == qDep){
            qDep--;
            pDep--;
            nowp = nowp->Fa;
            nowq = nowq->Fa;
        }
      }
      return nowp;
    }
    

    9. 有 n 个节点,m 条边,求最小生成树最长的边,n,m 都是100000以内。

    用小根堆优化 prim 算法。

    #include <bits/stdc++.h>
    using namespace std;
    const int INF=0x3f3f3f3f;
    const int N=201000;
    int n,m;
    
    struct edge{
    	int to,next,w;
    }e[N];
    int head[N],cnt;
    void add_edge(int u,int v,int w){
    	e[++cnt]=(edge){v,head[u],w};
    	head[u]=cnt;
    }
    int dis[N];
    bool b[N],vis[N];
    struct node{
    	int id;
    	node(int v){id=v;}
    	bool operator < (const node&b) const{
    		return dis[id]>dis[b.id];
    	}
    };
    priority_queue<node>q;
    int prim(){
    	int ans=0;
    	memset(dis, INF, sizeof(dis));
    	dis[1]=0;b[1]=true;
    	q.push(node(1));
    	while(!q.empty()){
    		int u=q.top().id;q.pop();
    		if(vis[u])continue;
    		vis[u]=true;
    		ans=max(dis[u],ans);
    		for(int i=head[u];i;i=e[i].next){
    			int v=e[i].to;
    			if(dis[v]>e[i].w){
    				dis[v]=e[i].w;
    				q.push(v);
    			}
    		}
    	}
    	return ans;
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	for(int i=0;i<m;i++){
    		int u,v,w;
    		scanf("%d%d%d",&u,&v,&w);
    		add_edge(u,v,w);
    		add_edge(v,u,w);
    	}
    	printf("%d",prim());
    	return 0;
    }
    

    10. n 个武器,告诉你每个武器的五个属性值,现在要选择 k 个武器,最大化这五个属性的最大值之和,n,k 都是10000以内。

    k>=5时,可以直接取每个属性的最大值。
    k<5时,我们用mask[i]表示 i 代表的二进制位上为1的所有位置对应的属性之和最大的一个武器(用 pair 记录<最大和,编号>)。接下来用 dp[i][j]表示前 i 个已经选了 j 二进制为1的位置对应的属性时,最大是多少。那么就有 dp[i][j|m]=dp[i-1][j]+mask[i].second,其中 m和 j 在二进制相同位置不能同时为1。

    #include <bits/stdc++.h>
    using namespace std;
    
    const int N=201000;
    int n,k,w[N][5],big[5];
    vector<pair<int,int> >mask(1<<5);
    int dp[5][1<<5];
    int main(){
    	scanf("%d%d",&n,&k);
    	for(int i=0;i<n;i++){
    		for(int j=0;j<5;j++){
    			scanf("%d",&w[i][j]);
    			big[j]=max(big[j],w[i][j]);
    		}
    	}
    	int ans=0;
    	if(k>=5){
    		for(int i=0;i<5;i++){
    			ans+=big[i];
    		}
    	}else{
    		for(int i=0;i<n;i++){
    			for(int j=0;j<(1<<5);j++){
    				int tot=0;
    				for(int t=0;t<5;t++)
    					if(j&(1<<t))
    						tot+=w[i][t];
    				mask[j]=max(mask[j], make_pair(tot,i));
    			}
    		}
    		for(int i=1;i<=k;i++){
    			for(int j=0;j<(1<<5);j++){
    				for(int m=0;m<(1<<5);m++)if((m&j)==0){
    					dp[i][j|m]=max(dp[i][j|m],dp[i-1][j]+mask[m].first);
    				}
    			}
    		}
    		ans=dp[k][(1<<5)-1];
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    11. 有20亿个无序存储的int32整数,找出一个不在里面的数。

    我的方法是将int32按区间划分为10万个组,每组4万多个数。第一遍扫描,维护每组数量:对每个数计算出它所在的组,然后该组数量+1。找出数量小于区间长度的一组,在第二次扫描时,只考虑在该区间内的数,存下来。接下来将存下的数排个序,再扫一遍这个排序的数组,如果相邻的总是连续的,那么若最小的大于左边界就返回左边界否则返回右边界。如果相邻的不连续就返回中间没出现的值。
    这样的时间复杂度是O(2*n+m+mlogm),n=2,000,000,000, m=int32范围/K≈40000,K=
    空间复杂度是O(K+m)

    在编程珠玑上看到了解法(题目类似):
    二分,位图的思想。
    第一次扫描,记录2进制第一位为0的个数,第一位为1的个数,
    如果0开头的少,第二次扫描,就只看00,01开头的。
    以此类推。要32次扫描就能确定这个数。
    时间复杂度是O(32*n),空间复杂度是O(1)。实现起来也更简洁。

    12. LRUCache(LeetCode146)

    面试完才知道这是道经常考的面试题,有原题,只不过把键值都改为字符串。
    当时写的 bug 太多了,对链表的操作也没有抽出函数来。回来又改了好久才 AC。

    需要实现一个 LRU Cache 的类, 总共最多维护 n 个键值对,并具有以下两个方法:
    put(k, v),不存在则插入,存在则更新键为 k 的值为v。
    get(k),读取键为 k 的值,不存在输出 -1。
    LRU 就是说,如果已经有 n 个了,那么再次插入时,要删去最久没有访问的。插入和读取都算是访问。
    两个操作复杂度都要求是 O(1)。
    插入是 O(1)的,就要求 O(1) 时间得到最久没有访问的。
    我们就想维护一个队头最新,队尾最旧的队列。
    如果拿双端队列,每次读取时,不能 O(1)地维护队头最新,队尾最旧的性质。
    需要一个可以 O(1)地将中间的元素拿到队头,又能 O(1)地访问任何一个值的数据结构。
    考虑用双向链表来存键值对,为了能 O(1)访问,再拿 hash map 来存每个键对应的链表节点的指针。

    class LRUCache {
    	struct Node {
    		Node(int _k, int _v){
    			k = _k;
    			v = _v;
    			pre = next = NULL;
    		}
    		int k, v;
    		Node* pre;
    		Node* next;
    	};
    	int n;
    	unordered_map<int, Node*> m;
    	Node* head, *tail;
    	int len;
            // 将 p 移到队头。
    	void move_to_head(Node*p){
    		if(p==head)return;//已经是队头。
    		if(head==NULL){//之前没有元素。
    			head=tail=p;
    		}else{
                            //如果 p 后面有元素,那么它前面的节点变成了 p 前面的节点了(也可能是 NULL)。
    			if(p->next) {
    				p->next->pre = p->pre;
    			}
                            //如果 p 前面有元素,那么它后面的节点变成了 p 后面的节点了。
    			if(p->pre) {
    				p->pre->next = p->next;
    			}
                            // 如果 p 本身就是队尾,那么新队尾是 p 前面的。这里链长一定大于1,否则 p 也会是队头,之前就返回了。
    			if(p==tail){
    				tail=p->pre;
    			}
                            // 这里就是 p 成为了新的队头。
    			head->pre=p;
    			p->next=head;
    			head=p;
    		}
    	}
    public:
    	LRUCache(int _n):n(_n), len(0){
    		head = tail = NULL;
    		m.clear();
    	};
    	void reset(int _n){
    		len=0;
    		n=_n;
    		head = tail = NULL;
    		m.clear();
    	}
    	void put(int k, int v) {
    		if(m[k]){
    			move_to_head(m[k]);
    			m[k]->v = v;
    		}else {
    			m[k] = new Node(k, v);
    			if(len<n){
    				move_to_head(m[k]);
    				len++;
    			}else {// len == n
                                    // 删掉最旧的。
    				m[tail->k]=NULL;
    				if(head==tail){//只有1个节点,直接删掉(n==1)
    					delete tail;
    					head=tail=NULL;
    				}else{//多个节点,要记得更新队尾指针。
    					Node*tmp=tail->pre;
    					delete tail;
    					tmp->next=NULL;
    					tail=tmp;
    				}
    				move_to_head(m[k]);
    			}
    		}
    	}
    	
    	int get(int k) {
    		if(m[k]==NULL) return -1;
    		move_to_head(m[k]);
    		return m[k]->v;
    	}
    };
    

    13. 给定一个数组和滑动窗口的大小,请找出滑动窗口滑动过程的所有最大值。

    双端队列

    14. 单链表上的快排

    To be continued...

  • 相关阅读:
    D
    pat 甲级 1003 Emergency(最短路计数)
    Goldbach
    pta 1148 Werewolf
    CodeForces
    pta 1144 The Missing Number
    2020团体程序设计天梯赛 L1-3 洛希极限
    2020团体程序设计天梯赛 L1-2 猫是液体
    2020团体程序设计天梯赛 L1-1 嫑废话上代码
    2020年团体程序设计天梯赛-题解
  • 原文地址:https://www.cnblogs.com/flipped/p/8542785.html
Copyright © 2020-2023  润新知