第2章 递归与分治思想
第2章 递归与分治思想
文章目录
- 第2章 递归与分治思想
- 一、递归
- 1.1 例4 归并排序、快速排序
- 1.2 归并排序:
- 1.3 快速排序
- 1.4 小q的数列
- 1.5 树
- 二、分治
- 2.1 FBI树
- 2.2 求最大子串和
- 2.3计算表达式
- 2.4 中序序列
- 2.5 平面最近点对问题
- 三、倍增 -- 直接从小问题合成大问题答案
一、递归
一定要有个递归的终止条件
1.1 例4 归并排序、快速排序
- 三种O(n^2)的排序:冒泡,选择,插入
- 三种不基于比较的排序:桶,基数,计数
- 归并排序,快速排序
1.桶排序:
排序过程:
(1)设置一个定量的数组当作空桶子;
(2)寻访序列,并且把记录一个一个放到对应的桶子去;
(3)对每个不是空的桶子进行排序。
(4)从不是空的桶子里把项目再放回原来的序列中。
对于重复出现的数字排序也可以在桶内类似于计数一样,对应数字的桶计数(桶是排序好的),然后按照桶逐个输出
2.计数排序:
可以看作是简易桶排序的基础上,用了前缀和。比如有有1~100排好序的一个数组,在1的位置记小于等于1的数有几个,2的位置记小于等于2的有几个,这样记好后,对于7马上就能知道排在7前面的有多少个数
3.基数排序:
对于数字较大的情况,此时开个相同大小的数组的开销比较大
按照个位桶排序后,再按照十位,百位桶排序
从上到下,从左往右的顺序
1.2 归并排序:
每次把待排序区间一分为二,将两个子区间排序,然后将两个已经排好序的序列合并
归并排序的复杂度为O(nlogn)
为什么?
归并排序的递归过程如下,该递归树的高度为log2n(计算过程:假设待排序的数组元素个数为n,设高度为x,x意味着n个元素需要连续二分x次才剩下1个元素,即n/2^x=1,x=log2n),每一层的总比较次数为n,所以时间复杂度为nlogn。
快速排序的分析过程类似。快速排序的平均时间复杂度同样为nlogn。假设在平均情况下每次选取的基准值均为该数组的中间值,因此每次都将数组分成两半,直到分割到只剩一个元素。假设n个元素平分了x次后只剩1个元素,则n/2^x=1,x=log2n。每次分割后的比较次数为n(参考上图),所以时间复杂度为nlogn。
原文链接:https://blog.csdn.net/weixin_38314865/article/details/113839571
看是不是O(n)简单的技巧是看是不是把元素只访问了一遍
代码实现:
#include<iostream>
using namespace std;
int n;
int a[1005];
//借助一个数组来存排序小的值
int b[1005]; void hebin(int l,int mid,int r)
{//左边的第一个指针 int q=l;//右边区域的第一个指针int p=mid+1; for (int i=l;i<=r;i++){if ((p>r) || q<=mid&&a[q]<=a[p]){b[i]=a[q];q++;}else{b[i]=a[p];p++;}}for (int j=l;j<=r;j++){a[j]=b[j];}
}void sort_gui(int l,int r)
{if (l==r){return;}int mid=(l+r)/2;sort_gui(l,mid);sort_gui(mid+1,r);hebin(l,mid,r);
}int main()
{cin >> n;for (int i=0;i<n;i++){cin >> a[i]; }sort_gui(0,n-1);for (int i=0;i<n;i++){cout << a[i];}return 0;
}
应用场景:
1.求逆序对个数
其实排序的过程都是在消除逆序对的过程,而冒泡排序等一次只能消除一个。
归并排序在合并的过程中其实就是在消除逆序对,只需要在上面代码中增加一条语句,当右边区间的被选为最小加进来的时候就是在消除逆序对,但是注意的一点是这个条件判断一定要加等于a[q]<=a[p]
#include<iostream>
using namespace std;
int n;
int a[1005];
int count=0;
//借助一个数组来存排序小的值
int b[1005]; void hebin(int l,int mid,int r)
{//左边的第一个指针 int q=l;//右边区域的第一个指针int p=mid+1; for (int i=l;i<=r;i++){if ((p>r) || q<=mid&&a[q]<=a[p]){b[i]=a[q];q++;}else{b[i]=a[p];//dioup++;//记录逆序对//如 1345 | 2233//排 12//别忘记这个+1count+=mid-q+1;}}for (int j=l;j<=r;j++){a[j]=b[j];}
}void sort_gui(int l,int r)
{if (l==r){return;}int mid=(l+r)/2;sort_gui(l,mid);sort_gui(mid+1,r);hebin(l,mid,r);
}int main()
{cin >> n;for (int i=0;i<n;i++){cin >> a[i]; }sort_gui(0,n-1);for (int i=0;i<n;i++){cout << a[i];}return 0;
}
1.3 快速排序
选择一个基准,将小于基准的放在基准左边,大于基准的放在基准右边,然后对基准左右都继续执行如上操作直到全部有序
注意的小细节
基准的位置也是参与交换的
代码实现:
#include<iostream>
#include<algorithm>using namespace std;
int a[1005];
int n;void quick_sort(int l,int r)
{//定义基准位置 int mid = (l+r)/2;int x=a[mid]; //定义两个区间的一头一尾的指针int q=l;int p=r;while(q<=p){//这里不能写等于号是为了mid也参与交换否则碰到13754这样的情况会变死循环 while(a[q] < x){q++;}while(a[p] > x){p--;}//保险起见还需要确认一次,其实我觉得不需要 if (q<=p){swap(a[q],a[p]);q++;p--;}}if (l<p){quick_sort(l,p);} if (q<r){quick_sort(q,r);}
}int main()
{cin >> n;for (int i=0;i<n;i++){cin >> a[i];}quick_sort(0,n-1);for (int i=0;i<n;i++){cout << a[i] << " ";}cout << endl;return 0;
}
应用场景:
求一个序列的第k小数,若数组大的话显然不能按照O(n**2)的方法进行排序。而且用快排的话对于第k个数不在的区间就可以不管。
代码实现:
该题由于输入数量大,普通的读入会造成runtime error。
这样就没事
#include<iostream>
#include<algorithm>using namespace std;
int a[5000005];
int n,k,t,result;int FindK(int l,int r,int k)
{if (l==r){return a[l];}//定义基准位置 int mid = (l+r)/2;int x=a[mid]; //定义两个区间的一头一尾的指针int q=l;int p=r;while(q<=p){//这里不能写等于号是为了mid也参与交换否则碰到13754这样的情况会变死循环 while(a[q] < x){q++;}while(a[p] > x){p--;}//保险起见还需要确认一次,其实我觉得不需要 if (q<=p){swap(a[q],a[p]);q++;p--;}}if (k<=p){return FindK(l,p,k);} else if (q<=k){return FindK(q,r,k);}else{//这部分属于排好序的,所以直接返回即可 return a[k];}
}int main()
{std::ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin >> t;while (t--){cin >> n >> k;for (int i=0;i<n;i++){cin >> a[i];}result = FindK(0,n-1,k-1);
// for (int i=0;i<n;i++){
// cout << a[i] << " ";
// }
// cout << endl;cout << result << endl;}return 0;
}
另外,题目给了快读读取的方式,这个只能读数字
inline int read(){int x = 0, f = 1;char ch = getchar();while(ch < '0' || ch > '9'){if (ch == '-')f = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){x = (x<<1) + (x<<3) + (ch^48);ch = getchar();}return x * f;
}
1.4 小q的数列
链接:https://ac.nowcoder.com/acm/contest/21763/1002
题目描述:
小q最近迷上了各种好玩的数列,这天,他发现了一个有趣的数列,其递推公式如下:
f[0]=0 f[1]=1;
f[i]=f[i/2]+f[i%2];(i>=2)
现在,他想考考你,问:给你一个n,代表数列的第n项,你能不能马上说出f[n]的值是多少,以及f[n]所代表的值第一次出现在数列的哪一项中?(这里的意思是:可以发现这个数列里某几项的值是可能相等的,则存在这样一个关系f[n’] = f[n] = f[x/2]+f[x%2] = f[x]…(n’<n<x) 他们的值都相等,这里需要你输出最小的那个n’的值)(n<10^18)
输入描述:
输入第一行一个t
随后t行,每行一个数n,代表你需要求数列的第n项,和相应的n'
(t<4*10^5)
题解:
上课老师的思路:
可以看到这道题卡时间卡的很紧,尤其是看到t的大小的时候cin和cout就有点危险了。可以看到每次函数的时候n在除半所以进行一次递归的操作还是可以接受的,可以用递归来实现函数,但是第二个问题就不能用递归来一个个找了。可以先看这个函数的规律,f[i%2]写成i%2是等价的(这样可以少一次递归)因为从两个if可以得到。
规律:
f(60)
=f(30)+0
=f(15)+0+0
=f(7)+1+0+0
=f(3)+1+1+0+0
=f(1)+1+1+1+0+0
=1+1+1+1+0+0
其实这就是60的二进制111100,加的结果就是60的二进制有几个1
一个数是否于1,跟它的二进制最后一位是否是1有关
这样下来以二进制的角度去看,除2的操作就是右移一位的操作(n>>1),综合下来看就是n的二进制有多少个1.
第二问由于问的是最小的,所以f(n)等于1的个数没有任何多余的0
这样就可以写为
n ′ = 2 f ( n ) − 1 (这是二进制的运算) n'=2^{f(n)}-1(这是二进制的运算) n′=2f(n)−1(这是二进制的运算)
等价于(1<<f(n) )-1(cpp里这样写后运算是二进制之后结果会自动转为int),这样就求到了有相同二进制1的个数的最小整数。
另外实现上要注意小细节,由于n是10**18次的起码有18个1因此,左移1的时候这个1要开longlong否则会左移到int的尽头时会溢出
#include<iostream>
using namespace std;int Fun(long long int n)
{if (n==1){return 1;}if (n==0){return 0;}return Fun(n/2)+n%2;
}int main()
{int t,fn,n1;long long n;cin >> t;while (t--){cin >> n;fn = Fun(n);//1LL是long long型的1 cout << fn << " " << ((1LL << fn)-1) << endl;}return 0;
}
1.5 树
二叉树的左右孩子是严格区分的不能随便交换
1、完全二叉树:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树。
2、满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树和满二叉树好用的地方,可以根据左子节点是父节点的2倍,右子节点的父节点2倍加1的特性,只需要数组就能存储并且不丢失关系
先序遍历:先根-左右
中序遍历:左-中根-右
后序遍历:左右-后根
层序遍历:
已知前序后序的情况下中序是不唯一的,因为前序是根左右,后序是左右根,因此左右是分不清的
例题:
求先序排列
#include<iostream>
#include<string>
using namespace std;string middle;
string back;//参数列表:前两个为中序排列序列的左右区间,后两个为后序排列序列的左右区间
void solution(int l1,int r1,int l2,int r2)
{if (l1>r1){return;}char root=back[r2];cout << root;//这里可以省略因为最后都会变成l1>r1情况,不省略的话可以提前结束 if (l1==r1){return;}int pos = -1;for (int i=l1;i<=r1;i++){if (middle[i]==root){pos=i;break;}}//递归左子树solution(l1,pos-1,l2,l2+(pos-1-l1));//递归右子树solution(pos+1,r1,l2+(pos-1-l1)+1,r2-1);
}int main()
{cin >> middle >> back;int n = middle.length();solution(0,n-1,0,n-1);cout << endl;return 0;
}
二、分治
合久必分,分久必合
2.1 FBI树
链接:https://ac.nowcoder.com/acm/contest/21763/1013
我们可以把由“0”和“1”组成的字符串分为三类:全“0”串称为B串,全“1”串称为I串,既含“0”又含“1”的串则称为F串。
FBI树是一种二叉树[1],它的结点类型也包括F结点,B结点和I结点三种。由一个长度为2N的“01”串S可以构造出一棵FBI树T,递归的构造方法如下:
(1)T的根结点为R,其类型与串S的类型相同;
(2)若串S的长度大于1,将串S从中间分开,分为等长的左右子串S1和S2;由左子串S1构造R的左子树T1,由右子串S2构造R的右子树T2。
现在给定一个长度为2N的“01”串,请用上述构造方法构造出一棵FBI树,并输出它的后序遍历[2]序列。
#include<iostream>
using namespace std;int n;
string s;char solution(int l,int r)
{if (l==r){if (s[l]=='0'){cout << 'B';return 'B';}else{cout << 'I';return 'I';}}int mid = (l+r)/2;//左子树char lchild=solution(l,mid);//右子树char rchild=solution(mid+1,r); //根(自己本身)if (lchild=='B' && rchild=='B'){cout << 'B';return 'B';}else if (lchild=='I' && rchild=='I'){cout << 'I';return 'I';}else{cout << 'F';return 'F';}}int main()
{cin >> n >> s;//这样写是不行的
// int N=2**n;solution(0,(1<<n)-1);cout << endl;return 0;
}
2.2 求最大子串和
若采用最原始的方式时间复杂度是O(n**2)因为要遍历起点再用前缀和相减
1.动态规划
2.把序列分成两部分
左边有最大和
右边有最大和
最大和在中间(此时以中间边界为起点对左边求出最大后缀和,右边求出最大前缀和)
实现还是按照递归实现的
此方法的时间复杂度为O(nlogn)
#include<iostream>
using namespace std;
int n;
int a[10005];int maxs(int l,int r)
{int mid=(l+r)/2;if (l==r){return a[l];}int ans=max(maxs(l,mid),maxs(mid+1,r));//当最大和在中间//求左边区域的最大后缀和int tmp=0,ll=0,rr=0; for (int i=mid;i>=l;i--){tmp+=a[i];ll=max(tmp,ll);} tmp=0;for (int i=mid+1;i<=r;i++){tmp+=a[i];rr=max(rr,tmp);}ans = max(ans,ll+rr);return ans;
}int main()
{cin >> n;for (int i=0;i<n;i++){cin >> a[i];}int res=maxs(0,n-1);cout << res << endl;c++return 0;
}
3.基于贪心一题思路的解法
贪心原题:
给定长度为n的整数数列ai,找出两个整数ai和aj(i<j),使得ai-aj最大。
解法:
#include<iostream>
#include<cstring>
using namespace std;
int n;
int a[10005];
int minn[10005];
int main()
{cin >> n;for (int i=0;i<n;i++){cin >> a[i];}memset(minn,0,10005);//求出最小后缀for (int i=n;i>=1;i--){minn[i]=min(minn[i+1]+a[i])}int maxx=-MAXINT;int ansfor (int j=0;j<n-1;j++){ans=a[j]-minn[j+1];if (ans>maxx){maxx=ans;}}return 0;
}
因此,本题同样道理找sum[j]-sum[i-1];
使得最大的组合,与上面思想一样有个小要求就是j>i-1即i在0到j-2的范围内,sum【i-1】在此范围求后缀min
for (int i=0;i<n;i++){for (int j=i+1;j<n;j++){sum[j]-sum[i-1];}
}
2.3计算表达式
可以不用数据结构那样使用后缀树和栈来解决(比较复杂)
新的一种简单的方法
#include<iostream>
#include<string>
#include<cmath>
using namespace std;
string s;int cal_num(int l,int r)
{//这个初始化很重要包含了当-90的情况,可以变为0-90 int num=0;for (int i=l;i<=r;i++){num*=10;num+=s[i]-'0';} return num;
}int solution(int l,int r)
{int cnt=0;//加减号的位置int pos1=-1;//乘除的位置int pos2=-1;//次方符的位置int pos3=-1;for (int i=l;i<=r;i++){if (s[i]=='('){cnt++;}if (s[i]==')'){cnt--;}//包括了((5+1)))+3) if (cnt<=0){if (s[i]=='+' || s[i]=='-'){pos1=i;}if (s[i]=='/' || s[i]=='*'){pos2=i;}if (s[i]=='^'){pos3=i;}}}if (pos1==-1 && pos2==-1 && pos3==-1){//((5+4)+6)if (cnt==0 && s[l]=='('){//去掉左右括号 return solution(l+1,r-1);}//((5+4)+(6*8)if (cnt>0 && s[l]=='('){return solution(l+1,r);}//((5+4)+6))))if (cnt<0 && s[r]==')'){return solution(l,r-1);}//当只剩下数字时return cal_num(l,r);}//处理pos有数字的情况if (pos1!=-1){if (s[pos1]=='+'){return solution(l,pos1-1) + solution(pos1+1,r);}if (s[pos1]=='-'){return solution(l,pos1-1) - solution(pos1+1,r);}}if (pos2!=-1){if (s[pos2]=='/'){return solution(l,pos2-1) / solution(pos2+1,r);}if (s[pos2]=='*'){return solution(l,pos2-1) * solution(pos2+1,r);}}if (pos3!=-1){return pow(solution(l,pos3-1) , solution(pos3+1,r)); }
}int main()
{cin >> s;int res=solution(0,s.length()-1);cout << res << endl;return 0;
}
在自己的编译器上各种例子都通过了,但奇怪的是牛客的编译器一直在报的错误。网上查了说可能是有return的语句少了else的情况,编译器觉得不安全
编译错误:您提交的代码无法完成编译
a.cpp:90:1: error: non-void function does not return a value in all control paths [-Werror,-Wreturn-type]
}
^
1 error generated.
解决了
在最后填个return 0;即可
2.4 中序序列
链接:https://ac.nowcoder.com/acm/contest/21763/1012
来源:牛客网
给定一棵有n个结点的二叉树的先序遍历与后序遍历序列,求其中序遍历序列。
若某节点只有一个子结点,则此处将其看作左儿子结点
解法:
先序:根左右
后序:左右根
因为有了第二个条件,意味着先序序列中,根的第二个元素一定是左子节点,这样在后序中左的最后一个元素确定了,根也知道就能知道右了。
#include <iostream>
#include <vector>
using namespace std;vector<int> res;
int n;class Solution {
public:/*** 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可** * @param n int 二叉树节点数量* @param pre intvector 前序序列* @param suf intvector 后序序列* @return intvector*/void deal(int pl,int pr,int sl,int sr,vector<int>& pre, vector<int>& suf){if (pl > pr){return;}if (pl==pr){res.push_back(pre[pl]);return;}int left = pre[pl+1];int pos=-1;for (int i=sl;i<=sr;i++){if (suf[i]==left){pos=i;}}//左deal(pl+1,pl+1+pos-sl,sl,pos,pre,suf);//根res.push_back(pre[pl]);//右 deal(pl+1+pos-sl+1,pr,pos+1,sr,pre,suf);} vector<int> solve(int n, vector<int>& pre, vector<int>& suf) {deal(0,n-1,0,n-1,pre,suf);return res;}
};int main()
{cin >> n;vector<int> pre(n,0);vector<int> suf(n,0);vector<int>::iterator it;for (int i=0;i<n;i++){cin >> pre[i];}for (int i=0;i<n;i++){cin >> suf[i];}
// for (it=suf.begin();it!=suf.end();++it){
// cin >> *it;
// }Solution sol;vector<int> final = sol.solve(n,pre,suf);
// for (int i=0;i<final.size();i++){
// cout << final[i] << endl;
// }for (it=final.begin();it!=final.end();++it){cout << *it << endl;}return 0;
}
2.5 平面最近点对问题
(先按照x轴排序)先按照x轴垂直的方向将平面分成一半,在左边区间和右边区间找两点之间的最小值记录下来(这个找的过程其实也是一个递归的过程因为实际这部分不要明确写实现的过程用本函数的递归返回左右区间的各最小值再取个min得到d),接下来与靠近中间分割线两边的点之间的距离与刚刚的最小值进行比较。至于中间分割线附近的最近点只要找在对纵轴方向排序后以类似两个for循环的方式固定一个点找在纵轴方向小于d的另个点,如果大于d则直接跳出循环这样下来的复杂度在O(Nlognlogn)
实现代码课上给了,暂时没自己写。有空再补
三、倍增 – 直接从小问题合成大问题答案
如a的b次方的模p,把b拆成m+n的形式,一般以2的幂次,这样算a的m次放后以a的m次方倍增到a的b次方
第2章 递归与分治思想相关推荐
- 算法设计与分析第2章 递归与分治策略
第2章 递归与分治策略 2.1 递归算法 递归算法:直接或间接地调用自身的算法. 递归函数:用函数自身给出定义的函数.两个要素:边界条件.递归方程 优点:结构清晰,可读性强,而且容易用数学归纳法来证明 ...
- 递归和分治思想及其应用
目录 递归和分治思想 一些实例 逆序输出字符串 查找数组元祖是否存在 汉诺塔问题 八皇后问题 更多: 递归和分治思想 如果可以使用迭代,尽量别使用递归.由编译原理可以知道,每次自调用的时候,计算机都需 ...
- 006.递归和分治思想
为什么80%的码农都做不了架构师?>>> 斐波那契数列的递归实现  迭代实现  递归   1.递归 定义 迭代使用的是循环结构. 递归使用的是选择结构. 优点 使用递归 ...
- (算法设计与分析)第二章递归与分治策略-第二节:分治和典型分治问题
文章目录 一:分治法基本概念 (1)基本思想 (2)适用条件 (3)复杂度分析 二:典型分治问题 (1)二分搜索 (2)大整数乘法 A:大整数乘法(Karatsuba算法) B:字符串乘法 (3)St ...
- 数据结构 - 迭代、递归和分治思想
栈和队列的应用 文章目录 栈和队列的应用 迭代-循环结构 递归-选择结构: 分治 迭代-循环结构
- 本文专注于lt;递归算法和分治思想[胖虎学习算法系列]
本文专注于<递归算法和分治思想> 初衷:博主看到网上有很多人贴出各种OJ上的AC代码,很多都会标注上"递归"两字 我刚开始学习递归算法和分治法的时候,苦于没有人写出递归 ...
- 分治思想应用:数学归纳法、递归、归并排序、MapReduce
跟黄申老师学数学系列02(python实现) 引言: 数学归纳法(Mathematical Induction).递归.归并排序(merge sort).MapReduce,这些方法或技术都基于一个重 ...
- c语言分治法求众数重数_五大常见算法策略之——递归与分治策略
递归与分治策略 递归与分治策略是五大常见算法策略之一,分治策略的思想就是 分而治之 ,即先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的 ...
- 保研机试——1基础算法(排序、哈希、模拟(日期、图形、查找、进制、字符串)、递归与分治、贪心)
写在前面的话:笔者在大三上学期(2022.9.20)对刷算法题基本为0基础,通过博客记录自己的学习过程,本人的学习计划为: 1.大三上学期:首先看<王道计算机机试考研指南>,着重看看保研机 ...
最新文章
- IMI装系统装到一半出错?
- 当下全球最炙手可热的八位少年创业者
- 公众号微信支付ios和android,【微信支付】
- css电视适配,CSS3 巨大的投影电视屏幕
- CoinU基本概念分享(什么是去中心化钱包、助记词丢失怎么办等)
- 以太坊怎么注册_以太坊2.0将至,牛市即将到来??
- 内核aio_今天来说说令人让人傻傻分不清的BIO,NIO,AIO
- 读博文学Android
- 画图板-- 中点算法画圆
- 查看Linux服务器raid信息笔记整理!
- 用maven编译spark2.1.0
- html实现点击直接下载文件-前端教程
- 流体动力matlab仿真,IND4动力总成丨基于MATLAB simulink的液力变矩器仿真建模
- 计算机系科学与技术调研报告,计算机科学与技术专业认识实习调研报告
- IDC销售系统前台模板知了云模板
- 【PageHelper分页】实现拦截pageNum和pageSize
- 不要为明天忧虑(10.14)
- mac系统下查看端口占用问题的解决方案
- 在java中 数组是作为_2.在Java中,数组是作为____来处理的。
- 腾讯云轻量服务器性能评测:配置 8核 16G 18M 带宽
热门文章
- 常见防火墙设置图文说明
- 利用邻接表创建无向图
- 安利!推荐20款Intellij IDEA中炫酷的插件!
- Ray Tracing in One Weekend(中文翻译)
- 命令行subl_使用Subl.exe从命令行打开Sublime文本(Windows)
- gcc 编译命令选项$@ $^ $
- python多维数组怎么降维_使用python实现多维数据降维操作
- suse 11 sp4配置yast源报错cannot access installation media(Medium 1),Check whether the server is accessibl
- 大型智慧校园商业级源代码+演示
- 基于Click-to-Run安装方式的Microsoft Office如何安装基于MSI的Office组件(Visio 2016等)