NOIP2013普及组复赛 解题分析

1.计数问题

算法分析

剥数。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;
int main() // 剥数  
{
	int n, x;
	scanf("%d%d", &n, &x);
	int sum = 0;
	for (int i = 1; i <= n; ++i)
	{
		int t = i;
		while (t)
		{
			if (t % 10 == x) ++sum;
			t /= 10;
		} 
	}
	printf("%d\n", sum);
	return 0;
} 

2.表达式求值

算法分析

有一个取巧的读入方式。仔细观察数据特点,第一个数据是数字,第二个是运算符,第三个也是数字,后面就是运算符和数字成对出现了。可以用 w h i l e + s c a n f while + scanf while+scanf读入不定个数的数据。读入的时候,遇到数字就入栈,遇到乘号就从栈顶弹出一个数和此时读入的数相乘,然后结果再入栈。因为只有加号和乘号,最后栈里的数据都是要相加的,加起来就是答案。每一步都要模10000。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stack>
#define ll long long int
using namespace std;
const int P = 10000;
stack<ll> s;
int main()  
{ 
	char c;
	ll a;
	scanf("%lld", &a);
	s.push(a % P);
	while (scanf("%c%lld", &c, &a) == 2) // 读入不定个数  
	{
		if (c == '*')
		{
			ll u = s.top(); s.pop();
			s.push(a % P * u % P);
		}else s.push(a % P);
	}
	ll ans = 0;
	while (s.size())
	{
		ans += s.top();
		ans %= P;
		s.pop();
	}
	printf("%lld\n", ans % P);
	return 0;
}

算法拓展

1.递归求中缀表达式。对于区间 [ l , r ] [l, r] [l,r],每次从右侧找第一个加号,左右两侧的结果加起来。如果没有加号,找右数第一个乘号,结果乘起来。如果加号和乘号都没有,说明这是一个数字,直接返回。

利用前缀和,迅速判断区间内是否有加号或乘号。有一个注意事项:加号和乘号最多有10万个,每个符号配一个数字,数字的字符个数未知,不好开数组。开小了会RE。开1000万能过。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <stack>
#define ll long long
using namespace std;
const int P = 10000;
char s[10000010];
int sumjia[10000010], sumcheng[10000010];
int dfs(int l, int r)
{
	if (sumjia[r] - sumjia[l-1] == 0 && sumcheng[r] - sumcheng[l-1] == 0)
	{
		int x = 0;
		for (int i = l; i <= r; ++i) x = x * 10 + s[i] - '0';
		return x % P;
	}
	if (sumjia[r] - sumjia[l-1] == 0) 
	{
		int i;
		for (i = r; i >= l; --i) if (s[i] == '*') break;
		return dfs(l, i-1) % P * dfs(i+1, r) % P;
	}else
	{
		int i;
		for (i = r; i >= l; --i) if (s[i] == '+') break;
		return (dfs(l, i-1) % P + dfs(i+1, r) % P) % P;
	}
}
int main()
{
	scanf("%s", s + 1);
	int n = strlen(s+1);
	for (int i = 1; i <= n; ++i)
	{
		if (s[i] == '+') sumjia[i] = sumjia[i-1] + 1;
		else sumjia[i] = sumjia[i-1];
		if (s[i] == '*') sumcheng[i] = sumcheng[i-1] + 1;
		else sumcheng[i] = sumcheng[i-1];
	}
	printf("%d\n", dfs(1, n));
	return 0;
}

2.中缀表达式转后缀再计算。能做,不过没有上述两个方法简单。

3.小朋友的数字

算法分析

这里面涉及到两个概念:特征值和分数。先求特征值,就是区间和。分两种情况,对于第 i i i个小朋友,他的特征值可以包含自身 i i i和不包含自身。包含 i i i的情况,就是之前解决过的最大子段和问题。不包含的话,直接使用上一个小朋友的数据即可。

t [ i ] t[i] t[i]:包含 i i i的最大字段和。
f [ i ] f[i] f[i] i i i的特征值。

ll minsum = 0;
t[1] = sum[1];
minsum = min(minsum, t[1]);
for (int i = 2; i <= n; ++i) 
{
	t[i] = sum[i] - minsum;
	minsum = min(minsum, sum[i]);
}
f[1] = t[1];
for (int i = 2; i <= n; ++i)
	f[i] = max(t[i], f[i-1]);

再求分数。方法类似。

s [ i ] s[i] s[i] i i i小朋友的分数。

最后输出的时候,不管正数还是负数,直接模P输出即可,不用做特殊处理。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long 
using namespace std;
int n, p, a[1000010];
ll sum[1000010], t[1000010]; // t[i]:[j, i]最大值,含i  
ll f[1000010]; // 特征值  
ll s[1000010]; // 分数  
int main()
{
	scanf("%d%d", &n, &p);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	for (int i = 1; i <= n; ++i) sum[i] = sum[i-1] + a[i];
	ll minsum = 0;
	t[1] = sum[1];
	minsum = min(minsum, t[1]);
	for (int i = 2; i <= n; ++i) 
	{
		t[i] = sum[i] - minsum;
		minsum = min(minsum, sum[i]);
	}
	f[1] = t[1];
	for (int i = 2; i <= n; ++i)
		f[i] = max(t[i], f[i-1]);
	
	// s
	s[1] = f[1];
	ll maxval = s[1] + f[1];
	for (int i = 2; i <= n; ++i)
	{
		s[i] = maxval;
		maxval = max(maxval, s[i] + f[i]);
	}
	
	maxval = -1e9;
	for (int i = 1; i <= n; ++i) maxval = max(maxval, s[i]);
	printf("%lld\n", maxval % p);
	return 0;
}

以上代码80分。原因是分数爆 l o n g   l o n g long \, long longlong。特征值会在 l o n g   l o n g long \, long longlong范围内,最大是 1 0 15 10^{15} 1015,分数的本质就是特征值的累加,最大可能会到 1 0 21 10^{21} 1021。那么,能否在上述过程中,边求分数边模P呢?不可以。连续模P得是在线性运算下,求最大值不是线性运算,不能模P。考虑用其他方法计算分数。

对于第 i i i个小朋友来说, s [ i − 1 ] s[i-1] s[i1]存的是 [ 1 , i − 2 ] [1, i -2] [1,i2]个小朋友特征值加分数的最大值,还需要和第 i − 1 i-1 i1个小朋友的分数加特征值进行比较。如果 f [ i − 1 ] < 0 f[i-1]< 0 f[i1]<0,则取 s [ i ] = s [ i − 1 ] s[i] = s[i-1] s[i]=s[i1],否则取 s [ i ] = s [ i − 1 ] + f [ i − 1 ] s[i] = s[i-1] + f[i-1] s[i]=s[i1]+f[i1]。因为这是线性运算,可以模P。

s[1] = f[1];
s[2] = s[1] + f[1];
for (int i = 3; i <= n; ++i)
{
	if (f[i-1] < 0) s[i] = s[i-1];
	else s[i] = s[i-1] + f[i-1];			
	s[i] %= p;
}

最后的结果不能是从前到后扫描取最大值,因为是模P下的。如果最后 s [ n ] > 0 s[n]>0 s[n]>0,结果就是 s [ n ] s[n] s[n],否则结果就是 s [ 1 ] s[1] s[1]。这题目坑还是很多的。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long 
using namespace std;
int n, p, a[1000010];
ll sum[1000010], t[1000010]; // t[i]:[j, i]最大值,含i  
ll f[1000010]; // 特征值  
ll s[1000010]; // 分数   s[i]会爆long long  
int main()
{
	scanf("%d%d", &n, &p);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	for (int i = 1; i <= n; ++i) sum[i] = sum[i-1] + a[i];
	ll minsum = 0;
	t[1] = sum[1];
	minsum = min(minsum, t[1]);
	for (int i = 2; i <= n; ++i) 
	{
		t[i] = sum[i] - minsum;
		minsum = min(minsum, sum[i]);
	}
	f[1] = t[1];
	for (int i = 2; i <= n; ++i)
		f[i] = max(t[i], f[i-1]);
	
	// s

	s[1] = f[1];
	s[2] = s[1] + f[1];
	for (int i = 3; i <= n; ++i)
	{
		if (f[i-1] < 0) s[i] = s[i-1];
		else s[i] = s[i-1] + f[i-1];			
		s[i] %= p;
	}
		
	ll maxval = s[1];
	if (s[n] > 0) maxval = s[n];
	
	printf("%lld\n", maxval % p);
	return 0;
}

4.车站分级

算法分析

从题意中可以看出:如果火车经过一个车站停了,那么下一个等级低的车站可停可不停,但是等级大于或等于他的一定停。所以从给出的停靠的车站推不出什么关系,但是起始站到终点站之间凡是没有停靠的车站,其等级一定低于停靠的车站。这样可以从等级低的向等级高的车站连边,转图论。求最长路。

可以用最长路算法求。因为本题中边权可理解为1,所以也可以用拓扑排序的思想。一层层的扫描,扫完一层,答案加1。最后也是最长路。图中不会出现环,有环的话,说明矛盾了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#define ll long long 
using namespace std;
struct node
{
	int next, to;
}edg[1000010];
int h[1010], cnt;
int n, m, vis[1010], sedge[1010][1010], sdu[1010];
vector<int> q, q1;
queue<int> s, s1;
void sadd(int u, int v)
{
	++cnt;
	edg[cnt].next = h[u];
	edg[cnt].to = v;
	h[u] = cnt;
}
int main()
{
	scanf("%d%d", &n, &m);
	int t, c;
	for (int i = 1; i <= m; ++i)
	{
		scanf("%d", &t);
		for (int j = 0; j < q.size(); ++j) vis[q[j]] = 0;
		q.clear();
		q1.clear();
		int st, ed; // 始发站  终点站  
		scanf("%d", &c); ++vis[c];
		q.push_back(c); 
		st = c;
		for (int j = 2; j < t; ++j)
		{
			scanf("%d", &c); ++vis[c];
			q.push_back(c);  // 停靠站  
		}
		scanf("%d", &c); ++vis[c];
		q.push_back(c); 
		ed = c;
		for (int j = st; j <= ed; ++j)
			if (!vis[j]) q1.push_back(j); // 非停靠站  
			
		for (int j = 0; j < q.size(); ++j)
		{
			int v = q[j];
			for (int k = 0; k < q1.size(); ++k)
			{
				int u = q1[k];
				if (!sedge[u][v])
				{
					sadd(u, v);
					++sdu[v];
					sedge[u][v] = 1;
				}
			}
		}
	}
	// 拓扑 队列  
	int ans = 0;
	for (int i = 1; i <= n; ++i) if (!sdu[i]) s.push(i);
	while (s.size())
	{
		++ans;
		while (s.size())
		{
			int u = s.front(); s.pop();
			for (int i = h[u]; i; i = edg[i].next)
			{
				--sdu[edg[i].to];
				if (!sdu[edg[i].to]) s1.push(edg[i].to);
			}
		}
		while (s1.size())
		{
			s.push(s1.front()); s1.pop();
		}
	}  
	printf("%d\n", ans);
	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值