说明:由于我能力的限制,文章中的做法不一定是最优秀的算法,但官方数据全部测试通过,使用的全部知识全部是NOIP提高组的知识,请组织放心查看。
感谢 GoodQt 的指导与帮助
DAY1 T1 玩具谜题
题目来源:洛谷 1563
吐槽:
对NOIP出题人“魔法师”英语单词拼错表示(滑稽)。
思路:
模拟操作过程,分类讨论即可。
时间复杂度:
O(n+m)
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node{
int dir;
char name[15];
};
node p[100010];
int n, m;
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) scanf("%d%s", &p[i].dir, p[i].name+1);
int now = 1;
for(int i = 1; i <= m; i ++){
int a, b;
scanf("%d%d", &a, &b);
if(p[now].dir == 0){
if(a == 0) now = ((now - b) % n + n) % n;
else now = (now + b) % n;
}else{
if(a == 0) now = (now + b) % n;
else now = ((now - b) % n + n) % n;
}
if(now == 0) now = n;
}
printf("%s", p[now].name+1);
return 0;
}
DAY1 T2 天天爱跑步
题目来源:洛谷 1600
吐槽:
这题的正解好神啊,巨坑,说好的NOIP按难度顺序出题呢?
思路:
读入第i个点的观察时间是
val[i]
。
通过DFS预处理,可以预处理得到每个节点的深度deep[i]和倍增需要的
up[Max][MaxLog]
数组,用倍增求得lca,在求得lca以后,我们可以把在lca路径上的点的操作转化为从起点和终点分别到根节点的操作-lca到根节点的操作-lca的父节点到根节点的操作。这样对于每一个跑步者的一个操作,我们把它分成了4个部分。
定义对与第
i
组询问,是从
具体的来说我们把问题转换成了从一个点
x
到根节点的路径中,所有满足
不难发现,一个节点
x
的答案被增加,只有可能受到以x为根的子树中某个点的影响。
而且需要满足的增加条件是定值(就是
时间复杂度:
O(nlogn+n)
(时间的瓶颈在求lca,lca如果用tarjan离线求可以做到并查集复杂度
O(αn)
)
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 300010;
struct node{
int begin, end, lca, len;
};
node p[maxn];
int n, m, val[maxn];
int _first[maxn], _next[maxn*2], _to[maxn*2], cnt;
int ins1[maxn], ins2[maxn], del1[maxn], del2[maxn];
int nxt[maxn*4], v[maxn*4], cnt1;
int deep[maxn], up[maxn][20];
int ans[maxn];
int ct1[maxn*2], ct2[maxn*2];
inline void add1(int a, int b, int c){
nxt[++cnt1] = ins1[a];
v[ins1[a] = cnt1] = c;
nxt[++cnt1] = del1[b];
v[del1[b] = cnt1] = c;
}
inline void add2(int a, int b, int c){
nxt[++cnt1] = ins2[a];
v[ins2[a] = cnt1] = c;
nxt[++cnt1] = del2[b];
v[del2[b] = cnt1] = c;
}
inline void add(int a, int b){
_next[++cnt] = _first[a];
_to[_first[a] = cnt] = b;
}
void dfs(int x){
for(int i = 1; i <= 18; i ++){
up[x][i] = up[up[x][i-1]][i-1];
}
for(int i = _first[x]; i; i = _next[i]){
if(_to[i] == up[x][0]) continue;
up[_to[i]][0] = x, deep[_to[i]] = deep[x]+1;
dfs(_to[i]);
}
}
void lca(){
for(int ii = 1; ii <= m; ii ++){
int x = p[ii].begin, y = p[ii].end;
if(deep[x] < deep[y]) swap(x, y);
if(deep[x] != deep[y])
for(int i = 18; i >= 0; i --)
if(deep[up[x][i]] >= deep[y]) x = up[x][i];
if(x != y){
for(int i = 18; i >= 0; i --)
if(up[x][i] != up[y][i]) x = up[x][i], y = up[y][i];
x = up[x][0];
}
p[ii].lca = x;
p[ii].len = deep[p[ii].begin] - deep[x] + deep[p[ii].end] - deep[x];
}
}
inline void dfs1(int x){
ans[x] = - ct1[deep[x]+val[x]] - ct2[deep[x]-val[x]+n];
for(int i = _first[x]; i; i = _next[i]){
if(_to[i] == up[x][0]) continue;
dfs1(_to[i]);
}
for(int i = ins1[x]; i; i = nxt[i]) ct1[v[i]] ++;
for(int i = del1[x]; i; i = nxt[i]) ct1[v[i]] --;
for(int i = ins2[x]; i; i = nxt[i]) ct2[v[i]+n] ++;
for(int i = del2[x]; i; i = nxt[i]) ct2[v[i]+n] --;
ans[x] += ct1[deep[x]+val[x]] + ct2[deep[x]-val[x]+n];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i < n; i ++){
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
deep[1] = 1, dfs(1);
for(int i = 1; i <= n; i ++) scanf("%d", &val[i]);
for(int i = 1; i <= m; i ++) scanf("%d%d", &p[i].begin, &p[i].end);
lca();
for(int i = 1; i <= m; i ++){
add1(p[i].begin, up[p[i].lca][0], deep[p[i].begin]);
add2(p[i].end, p[i].lca, deep[p[i].end]-p[i].len);
}
dfs1(1);
for(int i = 1; i < n; i ++) printf("%d ", ans[i]); printf("%d", ans[n]);
return 0;
}
DAY1 T3 换教室
题目来源:洛谷 1850
吐槽:
NOIP有史以来第一次考期望,对吧。
思路:
对于期望的概念,举例来说,已知A转换为B的代价为
p1
,现在有0.6的可能性让A转换为B的代价变为
p2
,那么A转换为B的期望代价就是
p1∗0.4+p2∗0.6
。进行期望计算的时候,一定要保证所有的项出现的概率之和为1。上面的例子中,
0.4+0.6=1
。
对于这个题,我们可以先用Floyd预处理每两个点之间的最短路,这样走一定是最优的。
剩下的部分可以用动态规划来实现,设dp[i][j][0/1]表示考虑前
i
个物品,已经换了
状态转移方程:
(太长了,好难看,看程序里的吧 ^_^)
其中
dis[i][j]
表示从
i
到
时间复杂度:
O(e3+nm)
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int inf = 2e9;
int n, m, v, e;
int a1[2010], a2[2010];
double dp[2010][2010][2], dis[310][310], w[2010], ans;
int main(){
scanf("%d%d%d%d", &n, &m, &v, &e);
for(int i = 1; i <= n; i ++) scanf("%d", &a1[i]);
for(int i = 1; i <= n; i ++) scanf("%d", &a2[i]);
for(int i = 1; i <= n; i ++) scanf("%lf", &w[i]);
for(int i = 0; i <= v; i ++)
for(int j = 0; j <= v; j ++)
if(i == j) dis[i][j] = 0;
else dis[i][j] = inf;
for(int i = 1; i <= e; i ++){
int a, b; double c;
scanf("%d%d%lf", &a, &b, &c);
dis[a][b] = min(dis[a][b], c);
dis[b][a] = min(dis[b][a], c);
}
for(int k = 1; k <= v; k ++)
for(int i = 1; i <= v; i ++)
for(int j = 1; j <= v; j ++)
if(dis[i][j] > dis[i][k]+dis[k][j])
dis[i][j] = dis[i][k]+dis[k][j];
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= m; j ++)
dp[i][j][0] = dp[i][j][1] = inf;
dp[1][0][0] = 0;
dp[1][1][1] = 0;
for(int i = 2, up; i <= n; i ++){
up = min(i, m);
for(int j = 0; j <= up; j ++){
dp[i][j][0] = min(dp[i][j][0], dp[i-1][j][0]
+dis[a1[i]][a1[i-1]]);
dp[i][j][0] = min(dp[i][j][0], dp[i-1][j][1]
+dis[a1[i]][a2[i-1]]*w[i-1]
+dis[a1[i]][a1[i-1]]*(1-w[i-1]));
if(j-1 >= 0) dp[i][j][1] = min(dp[i][j][1], dp[i-1][j-1][0]
+dis[a2[i]][a1[i-1]]*w[i]
+dis[a1[i]][a1[i-1]]*(1-w[i]));
if(j-1 >= 0) dp[i][j][1] = min(dp[i][j][1], dp[i-1][j-1][1]
+dis[a2[i]][a2[i-1]]*w[i]*w[i-1]
+dis[a2[i]][a1[i-1]]*w[i]*(1-w[i-1])
+dis[a1[i]][a2[i-1]]*(1-w[i])*w[i-1]
+dis[a1[i]][a1[i-1]]*(1-w[i])*(1-w[i-1]));
}
}
ans = inf;
for(int i = 0; i <= m; i ++) ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
printf("%.2lf", ans);
return 0;
}
DAY2 T1 组合数问题
题目来源:洛谷 2822
吐槽:
果然数学题隔一年出一次。我竟然因为开小数组挂了。
思路:
预处理杨辉三角,每次转移时把相加后的值对
k
取模,结果用二维前缀和记录。
时间复杂度:
代码:
#include <cstdio>
#include <iostream>
using namespace std;
int T, k;
int sum[2010][2010], val[2010][2010];
int a[10010], b[10010], p1, p2;
int main(){
scanf("%d%d", &T, &k);
for(int i = 1; i <= T; i ++){
scanf("%d%d", &a[i], &b[i]);
p1 = max(p1, a[i]);
p2 = max(p2, b[i]);
}
for(int i = 0; i <= p1; i ++){
for(int j = 0; j <= min(p2, i); j ++){
if(j == 0){val[i][j] = 1;continue;}
val[i][j] = (val[i-1][j] + val[i-1][j-1]) % k;
if(val[i][j] == 0) sum[i][j] = 1;
}
}
for(int i = 1; i <= p1; i ++){
for(int j = 1; j <= p2; j ++){
sum[i][j] = sum[i-1][j] + sum[i][j-1] + sum[i][j] - sum[i-1][j-1];
}
}
for(int i = 1; i <= T; i ++){
printf("%d\n", sum[a[i]][b[i]]);
}
return 0;
}
DAY2 T2 蚯蚓
题目来源:洛谷 2827
吐槽:
论巨慢的CCF评测机与巨慢的STL优先队列。
思路:
因为每一次蚯蚓的长度都会增加,而时间又不允许把所有的长度都进行增减,考虑到我们只需要知道所有蚯蚓的相对大小关系,所以把剩余的打标记,把新切出来的减去其余增加的长度,模拟每次操作。
如果用堆来维护大小的话,时间复杂度为
O((n+m)log(n+m))
实测手写堆65分,优先队列60分。
考虑优化。每次取出来切的蚯蚓一定是最长的一个,切完之后的新生成的蚯蚓不会比原来的蚯蚓长。基于这一点,我们考虑优化。如果我们现将输入的
n
个数按长度排序,然后把新生成的两个小的插入到输入的这
具体的,我们开两个队列(拒绝STL),分别维护每次分割完以后较长的一段和较短的一段的长度,加上输入序列,三个数列全部满足单调性。每次要切的蚯蚓从输入数列中和新开的两个队列中取最大值。实现的细节是要注意队列的边界,实现请参考代码。
时间复杂度:
O(nlogn+(n+m))
代码:
#include <cstdio>
#include <iostream>
#include <algorithm>
#define MAX(a,b) (((a)>(b))?(a):(b))
#define MIN(a,b) (((a)<(b))?(a):(b))
using namespace std;
inline int gt(){
char _ch = ' ';
int _num = 0, _op = 1, _ok = 0;
while(1){
_ch = getchar();
if(_ch == '-') _op *= -1;
else if(_ch >= '0' && _ch <= '9') _num = _num*10 + _ch - '0', _ok = 1;
else if(_ok) return _op * _num;
}
}
int n, m, q, u, v, t;
int left1;
int len[100010], q1[8000010], q2[8000010];
int out[1000010], cnt;
int main(){
scanf("%d%d%d%d%d%d", &n, &m, &q, &u, &v, &t);
for(int i = 1; i <= n; i ++) len[i] = gt();
sort(len+1, len+1+n);
int p1 = 1, p2 = 0, p3 = 1, p4 = 0, p5 = n, now = 0;
for(int i = 1; i <= m; i ++){
if(p5 >= 1 && len[p5] >= MAX(q1[p1], q2[p3])) now = len[p5--] + left1;
else if(p1 <= p2 && q1[p1] >= q2[p3]) now = q1[p1++] + left1;
else if(p3 <= p4) now = q2[p3++] + left1;
if(i%t == 0) out[++cnt] = now;
left1 += q;
int new1 = ((long long)now*(long long)u)/(long long)v;
int new2 = now - new1;
q1[++p2] = MAX(new1, new2) - left1;
q2[++p4] = MIN(new1, new2) - left1;
}
for(int i = 1; i <= cnt; i ++) printf("%d ", out[i]);
printf("\n");
for(int i = 1, w = 0; i <= n+m; i ++, w = 0){
if(p5 >= 1 && len[p5] >= MAX(q1[p1], q2[p3])) w = len[p5--] + left1;
else if(p1 <= p2 && q1[p1] >= q2[p3]) w = q1[p1++] + left1;
else if(p3 <= p4) w = q2[p3++] + left1;
if(i%t != 0) continue;
printf("%d ", w);
}
return 0;
}
DAY2 T3 愤怒的小鸟
题目来源:洛谷 2831
吐槽:
我在考前猜对了游戏,哈哈哈。正解是状压DP,又是第一次?
思路:
状态压缩动态规划。首先用18位二进制数表示,每一个是否被打了。
因为一直一定过原点,所以再有两个点就可以确定。
设另外的两个点为
A(x1,y1),B(x2,y2)
设抛物线的方程为
y=ax2+bx
带入方程:
y1=ax21+bx1
y2=ax22+bx2
上面乘
x2
,下面乘
x1
y1x2=ax21x2+bx1x2
y2x1=ax22x1+bx1x2
上式减下式:
y1x2−y2x1=a(x21x2−x22x1)
解得:
a=y1x2−y2x1x21x2−x22x1
b=y1−ax21x1
应该满足的条件是:
a<0
然后就可以预处理出
num[i][j]
表示以
i
为第一个点的第
然后就是动态规划了,设
dp[i]
表示现在的攻击状态二进制表示为
i
的最小次数。
dp[i|(1
<<
(now−1))]=min(dp[i|(1
<<
(now−1))],dp[i]+1)
其中,
now
是
i
这个状态中没有被攻击的最低的一位,因为考虑到在最优的答案中,这个位上一定会被攻击,所以只需要枚举攻击经过第
对于选定的第
时间复杂度:
O(Tn2n)
代码:
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const double eps = 0.000000001;
struct point{
double x, y;
};
int T, n, m, ans = 1e9;
point p[25];
int num[25][25], dp[300010];
int calc(double a, double b){
int res = 0, now = 1;
for(int i = 1; i <= n; i ++, now <<= 1){
double t = a*p[i].x*p[i].x+b*p[i].y;
if(abs(a*p[i].x*p[i].x+b*p[i].x-p[i].y) < eps){
res |= now;
}
}
return res;
}
int bits(int x){
int res = 0;
for(int i = 1; i <= n; i ++, x >>= 1) if((x&1) == 0) res ++;
return res;
}
int main(){
scanf("%d", &T);
while(T--){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++){
scanf("%lf%lf", &p[i].x, &p[i].y);
}
memset(dp, 63, sizeof(dp));
memset(num, 0, sizeof(num));
ans = 1e9;
for(int i = 1; i <= n; i ++){
for(int j = i+1; j <= n; j ++){
if(i == j) continue;
if(abs(p[i].x-p[j].x) < eps) continue;
double a = (p[i].y*p[j].x-p[j].y*p[i].x)/(p[i].x*p[i].x*p[j].x-p[j].x*p[j].x*p[i].x);
if(a >= 0) continue;
double b = (p[i].y-a*p[i].x*p[i].x)/p[i].x;
num[i][++num[i][0]] = calc(a,b);
}
}
int MAX = (1<<n)-1;
dp[0] = 0;
for(int i = 0; i <= MAX; i ++){
int now = 0;
for(int j = 1, w = 1; j <= n; j ++, w <<= 1){
if((w&i) == 0){now = j;break;}
}
if(now == 0) continue;
dp[i|(1<<(now-1))] = min(dp[i|(1<<(now-1))], dp[i] + 1);
for(int j = 1; j <= num[now][0]; j ++){
dp[i|num[now][j]] = min(dp[i|num[now][j]], dp[i] + 1);
}
}
printf("%d\n", dp[MAX]);
}
return 0;
}