[USACO1.5] [IOI1994]数字三角形 Number Triangles
题目描述
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 7→3→8→7→5 的路径产生了最大权值。
输入格式
第一个行一个正整数 r r r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例 #1
样例输入 #1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 #1
30
提示
【数据范围】
对于
100
%
100\%
100% 的数据,
1
≤
r
≤
1000
1\le r \le 1000
1≤r≤1000,所有输入在
[
0
,
100
]
[0,100]
[0,100] 范围内。
解题思路
要找出最大权值和,即最后一行最大的数字,就要找出它前一行的最优解,再找到前一行的前一行的最优解……直到递推到三角形的第一行。
选取“1”作为样本分析:
可以看到,1的权值和,是由3 8决定的。
将1表示为arr[i][j],3表示为arr[i-1][j-1],8表示为arr[i-1][j],就可得到状态转移公式:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
1
]
+
a
r
r
[
i
]
[
j
]
,
d
p
[
i
−
1
]
[
j
]
+
a
r
r
[
i
]
[
j
]
)
dp[i][j] = max(dp[i-1][j-1]+arr[i][j],dp[i-1][j]+arr[i][j])
dp[i][j]=max(dp[i−1][j−1]+arr[i][j],dp[i−1][j]+arr[i][j])
输出的时候也要注意,要循环最后一行,找出最大权值和。
AC代码
#include<iostream>
using namespace std;
const int N = 1e3+5;
int dp[N][N],arr[N][N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
cin>>arr[i][j];
}
}
dp[1][1] = arr[1][1];//第一个数字的最大权值和是它本身
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
dp[i][j] = max(dp[i-1][j-1]+arr[i][j],dp[i-1][j]+arr[i][j]);
}
}
int cnt = 0;
for(int i=1;i<=n;i++)
{
if(dp[n][i] > cnt)
{
cnt = dp[n][i];
}
}
cout<<cnt;
}
[NOIP1999 普及组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
样例 #1
样例输入 #1
389 207 155 300 299 170 158 65
样例输出 #1
6
2
提示
对于前
50
%
50\%
50% 数据(NOIP 原题数据),满足导弹的个数不超过
1
0
4
10^4
104 个。该部分数据总分共
100
100
100 分。可使用
O
(
n
2
)
\mathcal O(n^2)
O(n2) 做法通过。
对于后
50
%
50\%
50% 的数据,满足导弹的个数不超过
1
0
5
10^5
105 个。该部分数据总分也为
100
100
100 分。请使用
O
(
n
log
n
)
\mathcal O(n\log n)
O(nlogn) 做法通过。
对于全部数据,满足导弹的高度为正整数,且不超过
5
×
1
0
4
5\times 10^4
5×104。
此外本题开启 spj,每点两问,按问给分。
解题思路
这道题分为两问,我们先看第一问,很明显求最长不上升子序列。
(记住这个不上升!)
如果使用前文讲过的朴素单调递增/减子序列解法,100%TLE没跑。这时,回顾一下学习最长上升子序列时的奇技淫巧,单调栈优化。
每次加入一个数时,判断它是否比栈顶的数大:
是, 直接入栈
否, 二分查找lower_bound,替换掉那个位置的数。因为前面的数越大,后面的子序列的长度就会越长。不一定就是那个长度最大的最长上升子序列。
再看第二问:求最少的系统。
这个时候,我们不得不再介绍一个奇技淫巧,Dilworth定理。
狄尔沃斯定理(Dilworth’stheorem)亦称偏序集分解定理,是关于偏序集的极大极小的定理,该定理断言:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。
好啦,想必你也看不懂,通俗一点解释,就是题目的第二问,即最少的最长不上升子序列的个数,可以转换为求此序列的最长上升子序列的长度。
只需要将序列倒序遍历,用第一问的方法就可以实现求最长上升子序列的长度了。
AC代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e5+5;
int arr[N],stk[N];
//stk数组模拟栈,stk[i] 存长度为i的上升子序列的最小结尾
int lower_bound1(int stk[],int l,int r,int x)
{
while(l < r)
{
int mid = l+r >> 1;
if(stk[mid] > x)
{
r = mid;
}
else
{
l = mid+1;
}
}
return l;
}
int lower_bound2(int stk[],int l,int r,int x)
{
while(l < r)
{
int mid = l+r >> 1;
if(stk[mid] >= x)
{
r = mid;
}
else
{
l = mid+1;
}
}
return l;
}
int main()
{
int n = 0;
int x;
while(cin>>x) arr[++n] = x;//不能直接输入arr[++n]
// while(scanf("%d",%arr[++n]) != EOF); 另一种输入形式
int k = 0;
for(int i = n;i >= 1;i--)
{
if(!k || arr[i] >= stk[k]) stk[++k] = arr[i];
//如果数不在栈内,或是数不小于栈顶(因为不上升),入栈
else
{
int l = lower_bound1(stk, 1, k, arr[i]);
//否则二分查找大于等于它的最小值
stk[l] = arr[i];
}
}
cout<<k;
cout<<endl;
int j = 0;
for(int i = 1;i <= n;i++)
{
if(!j || arr[i] > stk[j]) stk[++j] = arr[i];
else
{
int l = lower_bound2(stk, 1, j, arr[i]);
stk[l] = arr[i];
}
}
cout<<j;
}