这里讲到的难题是一部分非常典型的题目,但并不是所有。因此这并非是一个全面的知识列表,而只适合提高组同学用来提升能力和拓展视野。
这篇文章在很多地方讲述的不够详细和严谨,因为它的作用并非是题解,而是更加深刻地理解题目的核心难点!
下面列举出的所有问题,一遍看不懂都是非常正常的,我挑选的题目肯定都是我一遍没能搞懂的,其中的重点都会在题目分析里清晰指出。如果看了题目分析后仍然搞不懂,请自行查阅相关题解。
P1020 [NOIP1999 普及组] 导弹拦截
题目分析:
第一问显然是求最长不上升子序列,二分优化即可(这个地方也算个不大不小的难点吧,自行查阅即可,不难)。
第二问需要用到 Dilworth 定理:一个序列最少的最长不上升子序列数量等于其最长上升子序列的长度。
证明如下:
假设我们已经将序列划分成了最少的几个最长不上升子序列,那么一定可以从每个中找到一个元素,新生成的序列一定单调上升(否则就最长不上升子序列的数量会减少),而且每个最长不上升子序列中只会被选出一个元素,否则单调性就会产生冲突,因此 Dilworth 定理是对的。
引用自 Apathy_Cui
如果不能理解 Dilworth 定理,也可以试试这种说法:
引用自 离散小波变换°
示例代码:
#include
#include
using namespace std;
int&merge(int f[],int l,int r,int x) {//在一个不升序列中找到第一个<=x的数字
if(l==r) return f[l];
int mid=l+r>>1;
if(f[mid]<x) return merge(f,l,mid,x);
else return merge(f,mid+1,r,x);
}
int main() {
const int N=1.1e5;
int a[N],f[N],n=1;
int x;
while(cin>>x) a[n++]=x;
n--;
for(auto&i:f) i=0;
int cnt=1;
f[cnt]=a[1];
for(int i=2;i<=n;i++) {
if(a[i]<=f[cnt]) f[++cnt]=a[i];
else merge(f,1,cnt,a[i])=a[i];
}
// for(int i=1;i<=cnt;i++) cout<
// cout<
cout<<cnt<<endl;
for(auto&i:f) i=0;
cnt=1;
f[cnt]=a[1];
for(int i=2;i<=n;i++) {
if(a[i]>f[cnt]) f[++cnt]=a[i];
else *lower_bound(f+1,f+1+cnt,a[i])=a[i];
}
cout<<cnt;
}
P1886 滑动窗口 /【模板】单调队列
题目分析:
题目分析假定你学习过单调队列的定义。如果你没有学过,董老师的视频非常简洁明了地讲解了单调队列(单调队列实际上维护了队列内元素的双重单调性,一般是值单调和下标单调)。
这个题目能用单调队列解决的关键在于:假设我们正在求max,如果y在x的后面,且y>x,那么x就永远没有机会成为max了,可以直接删去x。这和单调队列的特征是一样的。
其中,第一问和第二问维护的单调性相反,写完第一问之后不必再写第二问,直接把数组元素全部添上负号,再跑一遍第一问的单调队列,输出答案时再把负号删掉就好了。
单调队列最主要的难点在于代码不好写,结合下面的代码,我们可以总结一下,在for循环里面无非干了这三件事:
我写的进队函数(push_up,代码很久之前写的了,我也不知道为啥起了这个名字)里无非干了这两件事:
示例代码:
#include
#include
using namespace std;
struct coor {
int v;
int p;//这里v表示值,p表示下标
};
void push_up(deque<coor>&q,coor x) {
while(!q.empty()&&q.back().v>x.v) q.pop_back();
q.push_back(x);
}
int main() {
int n,m;
cin>>n>>m;
int a[n];
for(auto&i:a) cin>>i;
deque<coor> q;
int cnt=0;
for(int i=0;cnt<n;i++) {
while(q.empty()||q.back().p<i+m-1) push_up(q,coor{a[cnt],cnt++});
while(q.front().p<i) q.pop_front();
cout
// <
<<q.front().v
// <<"("<
<<' ';
}
cout<<endl;
q.clear();
for(auto&i:a) i=-i;
cnt=0;
for(int i=0;cnt<n;i++) {
while(q.empty()||q.back().p<i+m-1) push_up(q,coor{a[cnt],cnt++});
while(q.front().p<i) q.pop_front();
cout
// <
<<-q.front().v
// <<"("<
<<' ';
}
return 0;
}
其实这个代码是有很多细节的,比如||运算符两边的逻辑表达式位置不能调换,push_up函数必须传引用(这里不仅是需要修改q,也是确保时间复杂度)。
P5020 [NOIP2018 提高组] 货币系统
题目分析:
这道题的关键在于证明较大的货币系统A(n,a),和较小的货币系统B(m,b)等价,当且仅当b是a的子集。
如果不存在m
我们一条数轴表示货币系统A,在这样的数轴上,蓝点代表现有的货币面值,红点代表我们可以表示的货币面值。
有些现有的钞票又可以被更小的钞票表示出来,我在这样的蓝点周围打了红色。
显然,b中所有点要么是蓝点,要么是红点,如果b中的一个点既不是蓝色也不是红色,那么对于非负整数 x,它只能被B表示出来,A、B就不等价了。
更进一步,b中的点不可能带有红色。如果有,删去它结果一定更优。因为比它数值小的点在B中一定能被表示出来(因为A与B等价),所以它也能被更小的点表示出来,完全不需要它本身面值的货币了。
因此,这道题目就被转换成了完全背包统计方案数的题目,如果对于现有货币x,没有除了自己之外的任何方法能把它表示出来,那么这个货币就是必须保留的,反之它就是可有可无的。
为了防止方案数太多,我们把它改成“求价值为i的点,最多能够被几张钞票表示出来”,把结果存在f[i]里面。
示例代码:
#include
#include
#include
using namespace std;
int f[25000+5];
int a[105];
int main() {
int T;
scanf("%d",&T);
while(T--) {
for(auto&i:f) i=0;
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),f[a[i]]=1;
sort(a+1,a+1+n);
for(int i=a[1];i<=a[n];i++) {
int k=upper_bound(a+1,a+n+1,i)-a;
for(int j=1;j<k;j++)
if(f[i-a[j]])
f[i]=max(f[i],f[a[j]]+f[i-a[j]]);
}
int cnt=0;
for(int i=1;i<=n;i++)
if(f[a[i]]>1) cnt++;
printf("%d\n",n-cnt);
}
return 0;
}
整理一下代码思路:
P2014 [CTSC1997] 选课
题目分析:
这个题是非常典型的书上背包,直接上代码:
#include
#include
using namespace std;
int h[305];
bool vis[305];
vector<vector<int>>G;
int f[305][305];
//f[i][j]表示i节点中选修了j门课,
//并且选择i节点(不选那么f=0) 所获得的学分
int fa[305];
int n,m;
void dfs(int u) {
if(vis[u]) return;
vis[u]=true;
f[u][1]=h[1];
for(auto&v:G[u])dfs(v);
for(auto&v:G[u])
for(int k=1; k<=m; k++)
for(int x=1; x<k; x++)
f[u][k]=max(f[u][k],f[u][k-x-1]+f[u][1]+f[v][x]);
}
int main() {
cin>>n>>m;
vector<int> x;
for(int i=0; i<=n; i++) G.push_back(x);
for(int i=1; i<=n; i++) {
int u=i,v,w;
cin>>v>>w;
fa[i]=v;
h[i]=w;
G[v].push_back(i);
}
dfs(0);
cout<<f[0][m];
return 0;
}
转移方程:转移方程:f[u][k]=max(f[u][k],f[u][k-x-1]+f[v][x]+f[u][1]),意思很清晰。
但这个代码是错的,交到洛谷上零分。
感谢北京大学叶博文老师在此代码的基础上做了一定改进,成功通过该题:
#include
#include
using namespace std;
int h[305];
bool vis[305];
vector<vector<int>>G;
int f[305][305];
//f[i][j]表示i节点中选修了j门课,
//并且选择i节点(不选那么f=0) 所获得的学分
int n,m;
void dfs(int u) {
f[u][1]=h[u];
for(auto&v:G[u])dfs(v);
for(auto&v:G[u])
for(int k=m+1; k>=1; k--)
for(int x=0; x<k; x++)
f[u][k]=max(f[u][k],f[u][k-x]+f[v][x]);
}
int main() {
cin>>n>>m;
vector<int> x;
for(int i=0; i<=n; i++) G.push_back(x);
for(int i=1; i<=n; i++) {
int u=i,v,w;
cin>>v>>w;
h[i]=w;
G[v].push_back(i);
}
dfs(0);
cout<<f[0][m+1];
return 0;
}
这份代码与上面的代码有两个显著的区别,也揭示了上一份代码零分的原因: