算法课程笔记

大二下的算法笔记

有关快排的讨论

不用交换的做法

利用一个和数组等长的空间。

把数组扫一遍,若a[i]pivot则从后向前放。

随便选个pivot,要不就选中间的数。

1
2
3
4
5
6
7
8
9
int p=l,q=r-1,pivot=a[(l+r)/2];
while(p<=q){
while(a[p]<=pivot)++p;
while(a[q]>=pivot)--q;
if(p<=q){swap(p,q),++p,--q;}
}
if(p<r-1)qsort(p,r);
if(l<q)qsort(l,q-1);

大概这样。

设长为n的数组的时间复杂度是T(n);开始推导:

$T(n)=\sum_{i=1}^{n-1}P_i(T(i)+T(n-1-i))+O(n) \\ O(n)是~partition算法的耗时,设~pivotpos落在0到~n-1的概率均等。 \\ T(n)=\frac{2}{n}\sum_{i=1}^{n-1}T(i)+O(n)=\frac{2}{n}S(n-1)+O(n) \\ S(n)=\frac{n+1}{2}(T(n+1)-O(n+1)) \\ S(n)-S(n-1)=T(n) \\ 2T(n)=2S(n)-2S(n-1)=n(T(n+1)-T(n))+T(n+1)+2n+1 \\ (n+1)T(n+1)=(n+2)T(n)-2n-1 \\ T(n+1)-(n+1)(2n-1)=\frac{n+2}{n+1}(T(n)-(n+1)(2n-1))$

我推不出来了,反正是o(nlogn)。

找第k大的数

跑partition,得到pivotpos。

若pivotpos==k,则返回pivot;

若pivotpos<k,则递归搜右半边;

若pivotpos>k,则递归搜左半边;

找中间数

对一个数:若前面的数都比它小,后面的数都比他大,则它是中间数。

找出一个数组所有的中间数。

对每一个数a[i]维护一个max[i]一个min[i];

max[i]是从0到i(取到i)最大的数;min[i]是从n-1到i(取到i)最小的数;

从前向后扫一遍确定max,从后向前扫一遍确定min;

最后扫一遍,若max[i]==min[i],则a[i]是中间数。

注意:对数组进行排序,中间数排序前后位置不变,但排序前后位置不变的数不一定是中间数。12345,54321,3不是中间数。

找最大最小的数

递归终止条件:若区间里只有2个数,则比较出结果。

先partition得到pivot;

对左半边跑【找最大最小的数】,对右半边跑【找最大最小的数】,得到lmin,lmax,rmin,rmax;

比较得到整个数组的最大最小。

赛马问题

有n匹马,比赛方法是每5个赛一次。想找出最快的3匹马,怎么办?

递归终止条件:若n<=5,则赛一次就找出123。

首先赛n/5向上取整(将其设为k)次,找出每一次的123。

然后对这k场的第一,再跑一遍【3匹马解法】,找到它们中间的123。

1那一场的2和3是23候选,2那一场的第二是3候选。也就是:

1 2 3

2 3

3

1已经确定了。第二第三的候选正好5个,再赛一场就行了。

复杂度

一开始赛k场是o(n)。然后是T(n/5)。最后一场是o(1)。

T(n)=o(n)+T(n/5)+o(1)。累加,o(1)累加是logn。

o(n)累加,n+n/5+n/5/5+…=n/(1-1/5),还是o(n)?

那就o(n)了。

递归终止条件:若n<=5,则赛一次。

前面跟【3匹马解法】是一样的。只不过,是对这k场的第一跑一遍【5匹马解法】。

现在,我们对这k场的第一得到了12345。形势是:

1 2 3 4 5

2 3 4 5

3 4 5

4 5

5

1已经确定了。23赛一场也可以确定,和【3匹马解法】的赛法一样。

接下来,对各种23的情况一通讨论。就暴力讨论。最多再赛两次。

最近点对问题

k维空间内有n个点,找到距离最近的点对。

先nlogn排序,按x坐标排序。

递归终止条件:若区间里只有两个点,那就返回这两个点的距离。

mid=(xmax+xmin)/2,按照mid划分成左右两半。

对左右两半执行【最近点对解法】,得到左右两半的距离最小值。

令d为左右距离最小值的最小值。

考虑跨区间最近点对的可能性。若最近点对跨区间,则一个点在左边,一个点在右边。

无论如何,这两个点的横坐标离mid不应该超过d。

因此,对[mid-d,mid+d]里的点两两枚举,像二分图一样。比较这样得到的最小距离和d。

复杂度是nlogn。首先排序是nlogn。

然后递归深度是logn。去证每次合并的复杂度是o(n)。

感觉上最糟的情况是可以达到o(n^2)的,但事实上因为抽屉原理,有下面的结论:

考虑[mid-d,d]的点p,它的对应点q只可能在[mid,mid+d]*[yp-d,yp+d]上,这个区域里面最多有6个点,所以o(n)。

反证法,划成6个小长方形,如图所示: 若存在超过6个点,则必然有一个小长方形里面有2个(及以上)点,小长方形的对角线长度就是小于d的。 那么,左边的最近点对距离就是小于d的,推出矛盾。 https://blog.csdn.net/sinat_35678407/article/details/82874216

分形地对二叉树布线

对于一个高为h的满二叉树,将所有的节点布在一个网格纸的格点上,每个格点只能放一个节点。问怎样布线得到的图形所占的矩形面积(长*宽)最小?

分型。用四分来做。如图所示:

分形扩张的次数是h/2。长和宽都是o(2(h/2))的。因此面积是o(2h)的,大概。

我一开始的一点想法

一开始我觉得不能用分治做。考虑构造左子树和右子树的最优解,如果左子树占了某个格点,右子树就不能占这个格点了,即子问题之间是相互影响的。

换一个角度考虑:要想让左右子树不相互影响,应该怎么布线呢?可能会有帮助。

求凸包

给定平面上n个点,求一个最小的凸包,即一个顶点为给定点的凸图形,所有点要不在边上,要不在里面。

还是分治的思想,有点像最近点对。先把平面划分为左右两个半平面,对左右求凸包。

终止条件:若2个点或3个点。3个点显然三角形,或者正好共线就和两个点一样处理,总之把它们连起来。

现在合并左右的凸包。假设对于左右的凸包,我们已经维护了它们的一个逆时针方向的顺序的记录。

最后的大凸包由3部分组成:左凸包的左轮廓,右凸包的右轮廓,还有连接两个凸包的两条边。现在我们去找连接它们的边。

一个点在左边,一个点在右边。如果左边的凸包和右边的凸包都全部在这两个点连成的线的同一边,则这就是我们要找的线。能找到两条。

有这样一个结论:设右边的点为i。若i+1和i-1在连线同一侧,则右边的凸包就在连线同一侧了。

  • 若它们在同一侧。画出i到i+1和i到i-1的射线,它们围出一个区域。因为是凸包,所以右边所有的点都在这个区域里。这个区域在线的一侧,ok。
  • 若它们不在同一侧。即使某一侧只有i+1这一个点。如果将这条线作为轮廓,它就不是凸包了,会凹下去。如图。
  • 因此【在同一侧】和【选这条线】是充要的关系。

依靠已经维护好的顺序记录,对于每一个边都可以用o(1)的时间判断是否可以。这有点像最近点对,合并时并不是每一个点都要两两算距离,并不是每一个点都要判断是否在直线同一侧。

如果找到了这个边,就顺势修改顺序记录,然后再绕着右边的凸包继续找第二条边。其实感觉上用链表记录可能会更好一点。

但是我不知道怎么找边。o(n^2)枚举吗?

对于合并:首先在左凸包上找到那个x坐标最小的点,将其作为原点。站在这个点往右看,找到张角最大和最小的两个右凸包上的点。这一步o(n)。

张角以逆时针方向为正,可以用正切代替。

从原点开始迭代,顺时针迭代。站在原点看,若【左凸包顺时针相邻的那个点】的张角不如【右凸包张角最大点】的张角大,则原点与【右凸包张角最大点】连线。去连下面的线。

若【左凸包顺时针相邻的那个点】的张角比【右凸包张角最大点】的张角大,迭代原点,原点变到【左凸包顺时针相邻的那个点】,我们接下来站在这个点看。重复上一步,直到可以连线。

下面的线同理,把角的比较条件从大换成小。

不用分治。首先在所有点中找到那个x坐标最小的点,将其作为原点。这一步o(n)。

站在这个点往右看,找到张角最大和最小的两个点,连起来。这一步o(n)。

站在【刚刚找的张角最大点】上,继续找张角最大点。直到到达【所有点中x坐标最大的点】。

站在【刚刚找的张角最小点】上,继续找张角最小点。直到到达【所有点中x坐标最大的点】。

此时已经构造好凸包啦。每次迭代o(n),要迭代o(n)次,是o(n^2)。

棋盘覆盖

在一个2n*2n的棋盘上,有一个方格是奇异点。用L形的地砖来覆盖方格。

如果方格是2*2的,并且有一个奇异点,那么正好是L形,很好办。

如果是2n2n的,将其分为4块(2(n-1)2(n-1))的棋盘。在最中间填一个L形,保证每一个子棋盘都有一个奇异点,问题划分为4个小问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int count=1;
void fill(int x1,int x2,int y1,int y2,int xq,int yq){
if(x1+1==x2){
if(xq==x1&&yq==y2)
board[x1][y2]=board[x2][y2]=board[x2][y1]=count++;
else if ...;
return;
}
else{
int xmid=(x1+x2)/2,ymid=(y1+y2)/2;
if(x1<=xq<=xmid&&y1<=yq<=ymid){
board[xmid][ymid+1]=board[xmid+1][ymid]=board[xmid+1][ymid+1]=count++;
fill(x1,xmid,y1,ymid,xq,yq);
fill(x1,xmid,ymid+1,y2,xmid,ymid+1);
fill(xmid+1,x2,y1,ymid,xmid+1,ymid);
fill(xmid+1,x2,ymid+1,y2,xmid+1,ymid+1);
}else if ...;
}
}

循环赛日程表

有N=2^n个运动员,要求每个人都与别的N-1个人赛一次,只能赛N-1天,排一个日程表。

为使问题更对称,假设第0天每个人要与自己赛一场。现在是赛N天了。

我们搞一个N*N的数组,表示第j天第i个人的对手。

1-N/2的人和N/2+1-N的人在前N/2天和自己这一组赛,在后N/2天和另一组赛。

和另一组赛的日程,就是和自己赛的日程,对手编号平移N/2。

1
2
3
4
5
6
7
8
9
10
11
int ans[N][N];for(int i=0;i<N;++i)ans[0][i]=i;
void schedule(int x){
if(x==0)return;
schedule(x/2);
for(int i=0;i<x/2;++i)
for(int j=0;j<x/2;++j){
ans[i+n/2][j]=ans[i][j+n/2]=ans[i][j]+n/2;
ans[i+n/2][j+n/2]=ans[i][j];
}
}

线性时间选择

给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素。

其实前面好像写到排序里了,用partition方法。

1
2
3
4
5
6
7
8
int partition(int l,int r){
int pivot=a[l],pivotpos=l+1;
for(int i=l+1;i<=r;++i)
if(a[i]<pivot)swap(i,pivotpos),++pivotpos;
swap(l,pivotpos);return pivotpos;
// 现在,pivotpos之前都比它小,之后都比它大
}

所以pivot就是第pivotpos大的元素,如果pos>k,那么对(l,pivotpos-1)作partition;如果pos<k,(pivotpos+1,r);如果pos==k,直接返回pivot。

时间复杂度真的是线性的吗?如果每次都选到中位数,那么只会找logn次…等比数列求和,总的来说会扫n个元素,因此是线性的。

如果选不到中位数?就强行使pivot=中位数。

贪心法

寻找区间最大子集

给出n个区间[l_i,r_i],找出这些区间的一个子集,使得子集内的区间没有重合部分,并且数目最大。

按照r_i的大小(结束时间)排序,然后依次选择。如果下一个区间不与被选择的上一个区间重合,则选择它。

正确性证明

首先,一定要选排序后的第一个区间。如果选第一个还能选第二个,当然要选第一个了。

如果选了第一个就不能选第二个,那么在二选一里面还是要选第一个,结束的早后面就不那么被动了。

现在已经选了第一个。然后把和第一个冲突的区间去掉,得到新的序列,继续选第一个……

更严谨的一版:

某一个最优解里面一定包含第一个区间。反证法,假设最优解的第一个区间是第i个而非第一个。

如果选第i个之后还能选第1个,当然要选第一个了,这个最优解不是最优解,矛盾。

如果选第i个之后不能选第1个,为什么不选第一个呢,即使2-i的区间还是都不能选,至少i+1后的区间选择情况不会比最优解更差,毕竟1比i结束早。

然后把和第一个冲突的区间去掉,得到新的区间序列,继续选第一个……

最小生成树MST

prim和kruskal。

kruskal:不断地选择边权最小且不会与【已选择的边】成环的边。

prim:不断地选择边,选择从【已加入点的集合】连接到【未加入点的集合】的权值最小的边。

prim是直接反证就行了。设e是prim得到的 第一条 不在最优解中的边,最优解中连接prim的【已加入点集合S1】【未加入点集合S2】的边是w。把e加入最优解会得到一个环。

证明e比w好。S1连通,S2连通,直接把e换成w就会得到更好的MST。矛盾,得证。

要求无负权值边。

类似prim:不断地选择边,选择【未加入点的集合】中路径长度最小的边。贪心的规则变了。

路径长度:要求经过的点只能在【已加入点的集合】里。

基于这样的观察:全局最优由局部最优构成,若u到v的最优路径是u->…->a->…->v,则u到a的最优路径也是u->…->a。

因此,v的最优路径是【所有与v邻接的点的最优路径+它到v的边】中最优的。

然后我们反过来看:如何利用这个性质从原点开始迭代找最优路径??

所有与原点邻接的点的最优路径要不就是别的什么,要不就是它与原点的连边。

如果是别的什么,那这个路径的第一步肯定也是和原点的连边啊。

所以与原点邻接的点中【与原点连边】权值最小的点,它的最短路径就是它与原点的连边。把它加入集合S。

再往下看:所有与集合S邻接的点的最优路径要不就是别的什么,要不就是【它与某已经确定最短路径的点的连边+那个最短路径(S中)】。

对【与集合S邻接的点】中,【它与某已经确定最短路径的点的连边+那个最短路径】最小的点,设为v。

设这个路径是u->…->w->v。

这个就是它的最短路径了!为什么呢?如果它是别的什么u->…->a->…->v,那么从u到a里面一定有一段是在S中的。再不济也要从原点出发吧!

设这一段为u->…->b->c->…->a。可以知道u->…->b->c不如u->…->w->v短。任何别的路径都不如现在这条短。

给定一些字符和频率,用01对这些字符进行编码,希望使平均码长最小。

正确性证明

叶节点都是字符。内节点的值为两个子节点值的和。

我们要构造一个二叉树,使得所有内节点的值的和最小。

首先,构造的二叉树中,不会有度为1的节点。不然把它的子节点提上来,立刻得到更优的解。

然后,频率越低的点越深,不然可以把它与更深且频率更高的点互换,立刻得到更优的解。

这样,频率最低和第二低的字符在二叉树里是兄弟。它们俩的父节点值为它们俩的频率之和。

我们拿掉这两个节点,改放上他们的父节点。然后以此类推,就构造了huffman树。

大概这样吧……

分礼物问题(不是正确解法)

有n个礼物,每个价值是vi,分给两个人,问怎么分才能使两人得到的价值差距尽量小?

错误解法

  • 按价值从大到小排序;把第一个给A。
  • 如果A比B少,下一个给A;否则给B。

19,16,9,8,8,8,8;

A:19,8,8=35;

B:16,9,8,8=41;

其实可以:

A:19,9,8=36;

B:16,8,8,8=40;

19,8,8=35

16,11,8,8=43

19,11,8=38

16,8,8,8=40

现在贪心解法和的比是4了。我不太清楚是不是approximation。

多机调度(不是正确解法)

N个机器,M个任务,每个任务耗时ti,希望完成所有任务总时间最短。

  • 把任务按耗时从大到小排序;
  • 把下一个任务分给当前总完成时间最小的机器。

这不就是后面那个makespan吗……

背包问题

给出n个物品,每一个有体积ci和价值wi,背包容量为C。要求 Σc <= C。希望Σw最大。

www三步走方法:

第一步:以什么为输入比较合适?就是dp数组的维度。

第二步:有什么可能的子问题?

第三步:总问题和子问题有什么关系?

建dp数组dp[i][s],表示只从前i个物品中取,且取到的体积和为s时,最大的价值和。

这是神奇的思路啊,发现去决定第i个物品放不放,就要去观察前i-1个物品并且背包空间更小的情况。反正神奇。

初始化物品情况时,一定要从1而非0开始计数!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i){
for(int s=1;s<=C;++s){
//dp[i][s+c[i]]=max(w[i]+dp[i-1][s],dp[i-1][s+c[i]]);
dp[i][s]=dp[i-1][s];
if(s-c[i]>=0 && dp[i][s]<w[i]+dp[i-1][s-c[i]]) dp[i][s]=w[i]+dp[i-1][s-c[i]];
// 这两种都合理吗?或许是的
// 后一种是对背包容量进行遍历
// 下标要从1开始记录w[i]
}
}
// 若其实是装不满的,就正好错过去了呢?导致dp[n-1][C]=0。
// 不会的哦!由于它是向后递推,就是只要能放得下(s-c[i]>=0),就把它放进去,此时其实可能是放不满的。

// 更高更妙的解法,省空间
for(int i=0;i<n;++i)
for(int s=C;s>0;--s){
if(s-c[i]>=0&&dp[i][s]<w[i]+dp[i][s-c[i]]) dp[i][s]=w[i]+dp[i][s-c[i]];
// 是因为考虑dp[i][s]时要考虑dp[i][s'<s],希望此时dp[i][s'<s]的值没有被更新(还是旧值),所以s要从后往前遍历
}

完全背包

每一种物品有无数个。

这是我的想法:

还是for i从1到n,考虑前i个物品。

然后对于s从C到0。我们现在看第i个物品。

如果放不下,那么就i-1,这个没有问题。

如果放得下?放一个,效果更好了。能不能再放一个?

为什么放一个会效果更好?感觉推不出感性的结论,就是那种【放一个效果好,那么就应该使劲放直到放不下】。

那么我们就一个一个放,直到再放一个效果不如原来。

1
2
3
4
5
6
7
8
9
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
for(int s=C;s>0;--s){
for(int k=1;;++k)
if(s-k*c[i]>=0&&dp[s]<k*w[i]+dp[i][s-k*c[i]])
dp[s]=k*w[i]+dp[i][s-k*c[i]];
else break;
}

会不会有这样的情况。我们现在一个不放。放了一个,发现更好了。放了2个,发现不如1个好。放了3个,发现比1个好??

不会。把放3个的【放2个】换成【放1个的方法】,应该能立刻得到更优解,并且有趣的是这个最优解就是【放2个的方法】,还不如【放1个】好。

更高更妙的代码:从前向后遍历背包容量。

1
2
3
4
5
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
for(int s=c[i];s<=c;++s)
if(dp[s]<dp[s-c[i]]+w[i])dp[s]=dp[s-c[i]]+w[i];

多重背包

bounded knapsack problem。

听说要对【每一个约束维度】+【限制取前i个】进行dp。

比如说最多选M个物品:

1
2
3
4
5
6
7
8
9
10
11
12
13
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
for(int s=0;s<=C;++s)
for(int m=1;m<=M;++m)
if(s-c[i]>=0) dp[i][s][m]=max(dp[i-1][s][m],dp[i-1][s-c[i]][m-1]+w[i]);

// 尝试更高更妙
for(int i=1;i<=n;++i)
for(int s=C;s>=c[i];++s)
for(int m=M;m>0;--m)
dp[s][m]=max(dp[s][m],dp[s-c[i]][m-1]+w[i]);
// 貌似是都要逆向遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
memset(dp,0,sizeof(dp));
// 我的想法是,如果它的上一步不是装满的,那么推过来的这一步也不是合法的
for(int i=1;i<=n;++i){
for(int s=0;s<=C;++s){
dp[i][s]=dp[i-1][s]; // 这个是合法的,上一步会正好装满
if(s-c[i]>=0&&(s-c[i]==0||dp[i-1][s-c[i]]!=0))
dp[i][s]=max(dp[i][s],dp[i-1][s-c[i]]+w[i]);
// 若s==c[i],则更新后恰好只装第i件物品,合法的
// 若dp[i-1][s-c[i]]!=0,那么再装一个i也是合法的
// 注意,物品还是要从1开始计数
}
} // 这样或许可以?

// 听说这样比较麻烦,直接用-inf会更方便
for(int i=0;i<=n;++i)
for(int s=0;s<=C;++s){
dp[i][s]=-inf;
if(!s)dp[i][s]=0;
} // 听说要先这样初始化。对于s=0的时候,什么都不装就是装满,是合法的状态。
for(int i=1;i<=n;++i){
for(int s=1;s<=C;++s){
dp[i][s]=max(dp[i-1][s],dp[i-1][s-c[i]]+w[i]);
// 假设非-inf值是合法的,我们数归
// 若dp[i-1][s]合法,则合法,不然不合法
// 若dp[i-1][s-c[i]]合法,则合法,不然也不合法
// 在i=1时,若正好装满,则s==c[i],此时dp[i-1][s-c[i]]=0而非-inf。
}
}

求方案总数

1
2
3
4
5
6
7
8
9
10
memset(dp,0,sizeof(dp));
memset(c,0,sizeof(c));
for(int i=1;i<=n;++i)
for(int s=C;s>0;--s)
if(s-c[i]>=0&&dp[s-c[i]]+w[i]>dp[s])
dp[s]=dp[s-c[i]]+w[i],c[s]=c[s-c[i]]; // 放i更优,放i有多少种做法呢?
else if(s-c[i]>=0&&dp[s-c[i]]+w[i]==dp[s])
c[s]+=c[s-c[i]]; // 无论是放还是不放i,都有最优解,要把两种做法的数量加起来
// 如果放i还不如不放,那就和不放的时候(i-1)做法一样

二维背包

输出最优方案

1
2
3
4
5
6
7
memset(dp,0,sizeof(dp));
memset(plan,0,sizeof(plan));
for(int i=1;i<=n;++i)
for(int s=C;s>0;--s)
if(s-c[i]>=0&&dp[s-c[i]]+w[i]>dp[s])
dp[s]=dp[s-c[i]]+w[i],plan[s]=plan[s-c[i]]|(1<<i);

给出n个数,选某些数使得加和正好为C,可以做到吗?如何选?(给出的数不能重复用)

若dp[i-1][s]可以,或c[i]+dp[i-1][s-c[i]]可以,则dp[i][s]可以。

最长相同子序列

longest common sequence.

给两个字符串,返回最长相同子序列。只要是子序列就行了,不用连续。

dp[i][j]:取串1前i个字符,取串2前j个字符,最长是多长。

1
2
3
4
5
6
for(int i=1;i<=l1;++i)
for(int j=1;j<l2;++j){
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(s1[i]==s2[j])dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
}

www观察最优解:我们看串1,若1的最后一个与2的最后一个匹配,那么dp[i-1][j-1]+1。

若1的最后一个与2的前面的元素(不是最后一个)匹配,那么dp[i][j-1]。把2最后一个删了也没关系的吧。

同理,也要考虑dp[i-1][j];

当做到ij的时候,i-1已经做完了;i,j-1也刚刚做完。因此递推是可行的。

选最大价值不重区间集

题目和贪心的版本差不多。是有n个区间,每一个区间有一个价值wi,要求选价值最大的区间集,并且它们在时间轴上不冲突。

约定dp[s]是0到s时间段内可以选的最大价值和的区间。

考虑第i个区间。默认的情况是不选。

然后如果end[i]<=s&&dp[s]<dp[start[i]]+w[i],就dp[s]=dp[start[i]]+w[i]。

1
2
3
4
5
6
7
memset(dp,0,sizeof(dp));
sort(sec+1,sec+n+1); // 按照结束时间从早到晚排序
for(int i=1;i<=n;++i)
for(int s=C;s>0;--s)
if(sec[i].end<=s&&dp[s]<dp[sec[i].start]+sec[i].w)
dp[s]=dp[sec[i].start]+sec[i].w;

为什么是按照结束时间从早到晚排序呢?如果不是这样,递推合法吗?

我感觉现在这样的递推是合法的。

内层循环是遍历截止时间。

选第i门课不会影响start[i]之前的选课。start[i]之前的选课要在前面就被确定。要求所有在start[i]之前结束的课都被考虑过了。

那,结束时间早的课就要排在前面,所以就要按结束时间排序了。

分礼物问题

有n个礼物,每个价值是vi,分给两个人,问怎么分才能使两人得到的价值差距尽量小?

o(n*m),m是价值总和:也就是希望其中一人得到的价值与m/2尽量相近,维护dp[i]表示其中一人得到价值i的分法。然后考察每一个礼物,递推。是背包问题,价值即重量,背包容量为m/2。

o(n^3):维护一个集合(链表或bool,或int按位记录分法)表示可以得到的价值,最后找与m/2最相近的方案。如果m非常大,可以用这个方法。找与m/2最相近的方案:维护一个小于等于m/2的价值,遇到更接近m/2的方案就更新它。

感觉上,就是【找到价值与m/2尽可能相近的方案】思路的延伸。

如果要求一人只能拿n/2个礼物:

dp:维护dp[i][k]表示其中一人拿k个礼物得到价值i的分法,相当于限制背包物品个数。最后看dp[尽量靠近m/2][n/2]。o(n(n/2)m)。

o(n^3):链表或结构体数组(下标为价值),记录礼物个数信息。每次礼物个数=n/2时,才比较价值是否更接近m/2,更新那个最优方案。

上台阶问题

这是个大水题了。有n个台阶,我们一串可以走1或2阶,有多少种上台阶的方法?

dp[i]是前i个台阶的走法。dp[0]=0, dp[1]=1; dp[i]=dp[i-1]+dp[i-2]。嗯。

最长上升子序列

有名的LIS问题:一个序列的子序列是可以不连续的,上升要求严格单调上升(不能等于)。

普通解法:设dp[i]是以i结尾的LIS的长度。对dp[i],初始化为1,遍历前面所有元素,若有比它小的j就更新成dp[j]+1。o(n^2)。

更高更妙的解法:设dp[i]是长度为i的LIS的最后一个元素。对第k个元素,若大于等于dp[max_length]就dp[max_length+1]=k,若大于等于dp[j]且小于dp[j+1]就dp[j+1]=k,若比dp[1]小就dp[1]=k。

事实上啊,可以证明更高更妙的dp[i]是递增的数组。所以只会修改一个,修改一个就break出去。然后怎么修改这一个呢?二分查找!

此时复杂度已经是o(nlogn)!

最大子数组和

一个数列,求最大子数组和,这个【子数组】要求连续。

设dp[i]是以i结尾的最大连续子数组和。对第i个数来说,要不加入前面的子数组,要不自己作为开头起一个子数组。

dp[i]=max(dp[i-1]+a[i],a[i])。也就是若dp[i-1]<0,我就自己起头一个子数组。

回文最小划分次数

一个字符串s,切几刀才能让每个字串都是palindrome?如aab,切一刀,aa b。

居然是dp,可太神奇了。

记忆化搜索版本:

dp[i][j]=solve(i,j)是从i到j的字串的划分数。

dp[i][j]=min{solve(i,k)+1+solve(k+1,j)}。

如果ij是palindrome,solve=0,直接return;

自底而上递推版本:

dp[i][i+gap],gap从1到n-1。

买卖股票

有一个代表股票价值的序列a,如果第i天买入第j天卖出的话,可以挣a[j]-a[i]。只有一笔钱,钱已经在股市中就不能买入了,卖出才能再买入。

一直找最长上升子序列?然后把各个子序列进行【选最大价值不重区间集】?

对!太有道理了!

最少编辑次数

编辑多少次才能使两个字符串相等,编辑包含增删改3种操作。

还是dp。dp[i][j]表示【串1前i个】【串2前j个】的编辑距离。

若a[i]==b[j],dp[i][j]=dp[i-1][j-1];

不等:

把a[i]删掉 或 给b[j]后面加一个a[i]:dp[i][j]=dp[i-1][j]+1;

把b[j]删掉 或 给a[i]后面加一个b[j]:dp[i][j]=dp[i][j-1]+1;

把a[i]改成b[j]:dp[i][j]=dp[i-1][j-1]+1;

就是最长相同子序列的递推公式。

博弈论

优化问题:系统的utility;博弈问题:个体的utility。

均衡:我不会再改变策略了,反正怎么改变也不会比现在更好。大家都不改变。

utility:(u_i(a_i,a_{-i}));a_-i就是别人的策略。

voting game

假设第一排有10个座位,我们要选1个位置,对手要选1个位置。

对10个位置:离谁近就归谁。怎么选自己获得的位置最多?认为我们和对方同时选,选到同一个位置就平局。

在中间是最佳防守策略。貌似就是无论如何也不会比对方差的意思。

纳什均衡点。

所有玩家都不想改变自己当前的策略。

制胜策略

dominant strategy。

无论对方选择什么策略,我的这个策略总是最好的,相比于我的其他各种策略。相比均衡更强。

若 上:x,100;下:100,x;(两端路的通过代价,x是路上人数)总人数100。则上50人,下50人。

但是,若上下路的两段路之间有一个代价为0的高速公路,则x=100。

努力反而让局面变得更糟?

拍卖问题

有一个东西,拍卖方也不知道它的价格。希望设计一个策略,让理性人报出自己心目中的估价。

假设只有少数人知道它其实很值钱,少数人可以用相对低的价格赢得拍卖。现在看来好像就是要避免这种情况。

收益:若得到,ui=vi-c,估价-拍卖定价。若没得到,ui=0;

第一步,在纸上提交报价;

第二步,出价最高仍然赢;

第三步,付给我第二高的价格;

假设别人都定好将要出的价格了,我要定自己的价格。

若我的估价和别人的出价一起排序之后我的估价是第一,则直接出估价就能得到。没必要上调报价,因为反正付第二高的价格。不能下调,若下调就可能拿不到了,而拿到的收益肯定是正数。

若我的估价是第二,则上调之后要不拿不到,要不拿到了,但是这是要付原来那个第一的价格,那个价格比我估价高的……亏了。往下调无所谓,反正都是拿不到。

第三第四……的情况和第二一样。

只能得到第二高的价格+1(这时第二名如果继续加价就有可能亏)。拍卖方得到的收益差不多,但是不知道它的真实价格。

并且过程很慢。

P:多项式时间可解。

decision problem:比如说给出一些物品和背包大小,问装的价值能不能到达V,决策问题。

NP:在多项式时间内知道你给我的解是不是对/最优。

验证是否为因子和背包问题的decision problem(能不能达到V)都是NP问题。

归约reduction:A可以归约到B。B的一个特例是A。

approximation:对于没法在多项式时间内求出最优解的问题,NP难。

CIRCUIT-SAT(circuit satisfiability)是NP问题,所有NP问题都可以归约成它。

和它难度一样的问题还有无数个。NPC:circuit-sat属于它,所有属于它的问题都可以互相归约。

相关定义

感觉这个是可能考的。

  • P:可以在多项式时间内找到解法的问题;
  • NP:可以在多项式时间内确定一个答案是否正确的问题;
  • NPC:NP完全问题,是一类NP问题,所有NP问题都可以归约到它们。
  • NP hard:所有的NP问题都可以归约到它,但它不一定是一个NP问题。
  • 归约:A归约到B,A是B的特殊情况,会解B一定会解A。

knapsack

在ci vi属于整数时,是可以在多项式时间解决的。

但是实数呢?NP难。NP-hard。

贪心:

按照性价比选?无穷坏

比如C=1;

10^-7 1

1 10^7-0.0000001

很坏。

按价值最大贪心?无穷坏

C=1;

1 100

10^-7 60

……

10^-7 60

max一下上面两个算法

是可行的。那么c是多少?然而我们不知道OPT。

我们尝试寻找OPT的上界。c<=OPT上界/ALG<=OPT上界/ALG下界。放缩。

OPT(sigma)<=ALG1+vmax

ALG3>=(ALG1+ALG2)/2>=(ALG1+vmax)/2,因为vmax一定会被ALG2选。

那么,c<=2了……

OPT(sigma)<=ALG1+vmax怎么来的??

证明:设按照性价比贪心第一个装不下的物品的价值是vl,

则OPT<=ALG1+vl<=ALG1+vmax,就得到了。

设装入ALG1+vl所需的容量是Cl,那么这个方案就是OPT|容量=Cl。

OPT|容量=Cl > OPT|容量=C。

或者这样说,我们引入FOPT(fraction OPT,此时物品可以随便切割)。

OPT|容量=Cl > FOPT|容量=C > OPT|容量=C。

另一个神奇的做法

来源:https://www.cnblogs.com/cy0628/p/14023341.html;

输入ε>0, 令m=向上取整[1/ε]. 首先我们取 t=1:m 个物品(遍历Cnt种取法), 然后对剩下物品应用ALG1(性价比贪心).

它的近似比为1+ε。

均衡问题

makespan最大完工时间。

n个任务,每个任务有一个长度li,可以分到m台机器上,每台机器任务总长度是Li。

要min{max Li}。

贪心,动态调度思想,每次放到目前完工时间最小的那个机器上。

这个不是最优解,如按照1 10 100的顺序放:

100

1 10,现在是101。

其实是:

10

1 100,makespan是100。

现在开始放缩:

OPT >= max{(Σli)/m,lmax} >= ((Σli)/m+lmax)/2; 可以理解。

ALG <= (Σli)/m+lmax;

考虑最后一个,它肯定被放在目前最矮的机器上,这台机器是<= (Σli)/m的。

然后放上最后一个,最后一个比lmax要矮。就得到了。

万一最后一个放完了之后这个机器并不是最大makespan呢?

一些顾虑和我们的想法

应该是先按照从短到长排序然后再动态调度吧。

数归:假设m个机器时,对于k=1到n-1的情况,都有最后放任务的那个机器最长,证k=n。

我们已经按照任务长度对任务升序排序并重新标号,最长的任务是n。设放最长的任务的机器是m1。

反证法:设放完最长的任务n后,还有一个机器m2比m1更长。去推矛盾。

设m2顶端是任务j,m1放n前顶端是任务i。(若m2顶端无任务,则不可能比m1长。若m1的n下无任务,可以认为是长度为0的任务。)

若i比j先放,即i比j短。那么根据规则,m2放j之前比m1短(每次挑最短的放)。m2原来比m1短,m2再放一个j,m1再放一个n。显然m1长于m2,矛盾。

若j比i先放,即j比i短。那么根据数归,放i之后m1已经比m2长了,但是m2顶端就是以前放的j,再往m1上放n不符合规则,矛盾。

进而可推出结论,某机器顶端任务一定比另一个机器非顶端任务后放。即,往某一机器放两个任务的间隔中,其他机器一定都放过任务了。

数归:一定是一轮一轮放任务

假设前m个任务按照1到m的顺序放,这个没问题。

设现在放km+x个任务,那么应该放在第x个机器。

如果放在第y个机器,y<x。y上已有k+1个任务,x上有k个任务,y的顶端k个任务比x的顶端k个任务每个长度都大,y还多最底端那个任务。y的长度大于现在的x(未放km+x的x)。

如果放在第y个机器,y>x。y的k个任务比x的k个任务每个长度都大。y的长度大于现在的x(未放km+x的x)。

所以只能把km+x放在x上。

若x=m呢?同理只能放在第m个机器,只是不用考虑“y>x”了。

在线算法online

若信息是一点一点被告诉的。

假设有一堵很长的城墙,我站在原点,知道某一个地方有一个门,去找门。

先往右走1,然后回到原点,然后往左走2回到原点,然后往右走4回到原点,往左走8回到原点,…

c可以到12。

牌面值为1到13(可重复),陆续发牌给我看,我决定要不要这张牌。我只能要3张。目标是最大化a1+a2+a3;

对任意情况都有OPT/ALG<=c。要考虑比较极端的情况。

第一张不会是13或1。最后一张给1。

解法

min{13/ALG, ALG/1},在一张牌的情况下。如果我要了这一张,接着出13。如果不要,最后出1。

sqrt(13)。若比sqrt13大,就拿。若不大于sqrt13,等。

所以3张牌怎么办??每次发牌都是独立的吧,大概。

阿尔卑斯山滑雪

买或租滑雪装备。买15k,租1k/天。

解法

租到第15天,然后买设备。c=2。

天数小于15时,我们是OPT。天数大于15时,c=2。

关灯问题

随机叫同学进一个屋子,屋子里有k个灯。我们有n个同学。

灯在外面是看不见的。同学在屋外不能交流。

每个同学进屋之后可以操作灯,下一个同学会看到操作后的状态。

问:什么时候可以确定每个同学都进屋一次?

首先:若一个同学第二次进入屋子,不要改变灯的状态。

设log2(n)向上取整个灯,做二进制加法。

一个灯的情况:事先商量一个关灯的同学。

一个不关灯的同学进入屋子,若:1. 灯是关的;2. 自己原来没开过灯,则开灯;

若关灯同学进屋时发现灯是开的,则关灯,count++。count一开始是0。

若count==n-1,则每个同学都进屋一次。