NOIP2018 复盘
NOIP2018 复盘
前言
在这里立一个可能无法实现的flag:
把NOIP从古至今(luogu上有)的每一年都写一篇复盘!!!
伏拉格综合征开始了
在复盘就不讲那些伤心的话了。
D1T1 铺设道路
考试时居然不知道这道题是原题。。。
一共有两种做法:
-
递推/贪心。
设一个数组\(f\),顺序遍历。是这么更新的:
\[f[i]=f[i-1]+max(0,a[i]-a[i-1]) \]反正我没做过原题想不出来 -
分治。
弄一个递归的函数,暴力统计区间最小值,暴力区间减,再来一个遍历找出断点,把所有的答案加起来就完事了。
但是据说这种做法是会被卡成\(O(n^2)\)的。但是幸好NOIP的数据没卡。
不然我就省四退役了
代码:(会被卡的做法2)
#include<cstdio>
#include<algorithm>
#define ll long long
const ll maxn = 100005;
ll a[maxn], n;
ll solve(ll l, ll r)
{
if(l > r) return 0;
ll ans = 0, minv = 0x3f3f3f3f;
for(ll i = l; i <= r; i++) minv = std::min(minv, a[i]);
for(ll i = l; i <= r; i++) a[i] -= minv;
ans += minv;
ll pos = l;
while(a[pos] != 0 && pos <= r) pos++;
if(pos == r + 1) return ans;
ans += solve(l, pos - 1);
ans += solve(pos + 1, r);
return ans;
}
int main()
{
//freopen("test.in", "r", stdin);
scanf("%lld", &n);
for(ll i = 1; i <= n; i++) scanf("%lld", &a[i]);
printf("%lld\n", solve(1, n));
return 0;
}
D1T2 货币系统
最初的想法是在一个大一点的范围内看看表示的会不会一样多。
不知道为什么就发现:只要看看能否表示出给你的所有货币。
从小到大排序,选到能表达出所有货币为止。
表示方法有两种:
-
dfs暴力搞。
暴力枚举出每一个数前面乘的数,看看能否表达就是了。
但是这种做法因为效率不高而只能搞80pts。
-
dp背包方案。
因为任何一种货币都能选到够,所以这不就是完全背包吗?
所以使用完全背包,从能够表达的状态转移到另一个状态即可。
同时,这个dp数组是可以循环利用的。如果每次枚举选几种货币的话会T掉。
这个就是正解了。
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
const int maxa = 25005, maxn = 105;
bool dp[maxa];
int a[maxn];
int n, ans;
bool check()
{
for(int i = 1; i <= n; i++)
{
if(!dp[a[i]]) return false;
}
return true;
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
//clearlove();
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
std::sort(a + 1, a + n + 1);
memset(dp, 0, sizeof dp);
dp[0] = 1;
ans = 0;
for(int i = 1; i <= n; i++)
{
if(dp[a[i]]) continue;
for(int j = a[i]; j <= 25000; j++)
{
dp[j] |= dp[j - a[i]];
}
ans++;
if(check())
{
printf("%d\n", ans);
break;
}
}
}
return 0;
}
D1T3 赛道修建
updated in Feb. 6th 2019.
这道题大致思路十分清晰:二分答案!二分枚举那个最小长度。
先看所有的部分分:
-
\(m=1\):直接求树的直径即可。20pts到手
-
\(b_i=a_i+1\):一条链。同P1182:二分答案之后扫一边数组来check。
-
\(a_i=1\):菊花图。这是一个比较重要的思路,会这个思路就能做题了:
如果图是菊花图,那至少取一条边,顶多取两条边。
大思路依旧是二分答案。设当前需要判定答案为\(mid\)时满不满足。
把所有边的权值都挑出来排个序。大于等于\(mid\)的边单独成赛道,小于\(mid\)在里面找。用贪心的思想,这个被配对的权值越小越好。
代码实现等会再告诉你。。。
之后就是正解了:
二分最小赛道长度,设要判定答案为\(mid\)。
随便弄个点当根,设\(f(i)\)为以\(i\)为根的子树中未匹配的经过\(i\)的最长路径。
给每个点都建立一个multiset,用来最大匹配所有的半赛道。
为什么要每个点建一个?两个经过同一个点的半赛道才能合成一个完整赛道!
在dfs时,设遍历到\(v\)点,\(f(v)+weight(u,v)\)与\(mid\)分两类讨论:
- 当\(f(v)+weight(u,v) \geq mid\)时,赛道数++。
- 否则,放入\(u\)点的multiset里面维护。
dfs完之后遍历每个点的multiset,每一次在里面找出最小权值,lower_bound出对应的iterator,如果满足条件就把两个半赛道都删掉,然后赛道数++;否则把这条半赛道抛弃掉,因为这条赛道没办法与剩下的任何赛道匹配。
最大的赛道数判断是否能够大于等于\(m\)即可,这样就能check出答案来了。
代码:(里面关于multiset的运用有得学)
#include<bits/stdc++.h>
#define ll long long
const int maxn = 50005;
std::vector<std::pair<int,int> > G[maxn];
std::multiset<int> s[maxn];
int n, m;
// get the diameter of tree
int dep[maxn];
void bfs(int s, int &maxdep, int &idx) {
std::queue<int> q;
memset(dep, -1, sizeof dep);
dep[s] = 0; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
if(dep[u] > maxdep) {
maxdep = dep[u]; idx = u;
}
for(auto it : G[u]) {
int v = it.first, w = it.second;
if(dep[v] == -1) {
dep[v] = dep[u] + w; q.push(v);
}
}
}
}
int get_diameter() {
int maxdep = -1, idx = -1;
bfs(1, maxdep, idx);
int temp = idx;
maxdep = idx = -1;
bfs(temp, maxdep, idx);
return maxdep;
}
int res;
int dfs(int u, int f, int mid) {
s[u].clear();
for(auto it : G[u]) {
int v = it.first, w = it.second;
if(v == f) continue;
int temp = dfs(v, u, mid) + w;
if(temp >= mid) {
res++;
} else {
s[u].insert(temp);
}
}
int ret = 0;
while(!s[u].empty()) {
int temp = *s[u].begin();
if(s[u].size() == 1) {
ret = std::max(ret, temp);
break;
}
auto it = s[u].lower_bound(mid - temp);
if(it == s[u].begin() && s[u].count(*it) == 1) ++it;
if(it == s[u].end()) {
ret = std::max(ret, temp);
s[u].erase(s[u].find(temp));
} else {
res++;
s[u].erase(s[u].find(temp));
s[u].erase(s[u].find(*it));
}
}
return ret;
}
bool check(int mid) {
res = 0;
dfs(1, 0, mid);
return res >= m;
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 1; i < n; i++) {
int u, v, w; scanf("%d %d %d", &u, &v, &w);
G[u].push_back(std::make_pair(v, w));
G[v].push_back(std::make_pair(u, w));
}
int left = 0, right = get_diameter(), ans = -1;
while(left <= right) {
int mid = (left + right) >> 1;
if(check(mid)) ans = mid, left = mid + 1;
else right = mid - 1;
}
printf("%d\n", ans);
return 0;
}
D2T1 旅行
这道题让我认识了什么叫做基环树!
这道题就是两个部分分:\(m=n-1\)或\(m=n\)。
\(m=n-1\)部分明显就是一棵树,那需要看怎么遍历这棵树才能得到答案。
仔细观察可以发现:把所有的边从小到大排序,然后从1节点开始遍历即可。60pts到手!
剩下的\(m=n\)部分分就是重点了。
基环树有这么几个性质:
- 基环树断了一条边可能就是一棵树。
- 基环树有且只有一个环。
再看到数据范围:\(1 \leq n \leq 5000\)!
结合去年的i7 8700k,不由得让你想到了\(n^2\)算法!
所以算法出来了:枚举所有的边,每次断掉其中的一条边,在新图上面dfs,求出最小字典序的答案。
还想优化?先跑一遍tarjan的点双,若一条边的两个端点属于同个点双即为环上的边,在上面断边,会去掉那些不是树的断法。
在luogu上面这两种做法开O2都是能过的。
加强版不会做
代码:
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
#define ll long long
#define pii pair<int,int>
const int maxn = 5005;
std::vector<int> G[maxn];
bool vis[maxn];
int n, m;
int dfn[maxn], low[maxn], dtot;
int col[maxn], ctot;
std::stack<int> sta;
std::vector<int> answers, results;
std::pii edges[maxn];
bool zidianxu() {
if(answers.size() == 0) return true;
for(int i = 0; i < n; i++) {
if(results[i] < answers[i]) return true;
if(results[i] > answers[i]) return false;
}
return false;
}
void dfs(int u, int f, int nou, int nov) {
results.push_back(u);
for(auto v : G[u]) if(v != f) {
if(u == nou && v == nov) continue;
if(v == nou && u == nov) continue;
dfs(v, u, nou, nov);
}
}
void tarjan(int u, int f) {
dfn[u] = low[u] = ++dtot;
sta.push(u); vis[u] = true;
for(auto v : G[u]) {
if(v == f) continue;
if(!dfn[v]) {
tarjan(v, u); low[u] = std::min(low[u], low[v]);
} else if(vis[v]) low[u] = std::min(low[u], dfn[v]);
}
if(dfn[u] == low[u]) {
// print
//cout << "circle is below:" << endl;
ctot++;
while(sta.top() != u) {// only enter here is circle
int sb = sta.top(); sta.pop(); vis[sb] = false;
col[sb] = ctot;
//cout << sb.first << ' ' << sb.second << endl;
}
int sb = sta.top(); sta.pop(); vis[sb] = false;
col[sb] = ctot;
//cout << sb.first << ' ' << sb.second << endl;
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
G[u].push_back(v); G[v].push_back(u);
edges[i] = std::make_pair(u, v);
}
for(int i = 1; i <= n; i++) std::sort(G[i].begin(), G[i].end());
if(m == n - 1) {
dfs(1, 0, 0, 0);// tree don't need vis
if(zidianxu()) answers = results;
} else if(m == n) {
for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, 0);
//for(auto it : circle) cout << it.first << ' ' << it.second << endl;
// change one edge
for(int i = 1; i <= m; i++) {
if(col[edges[i].first] == col[edges[i].second]) {
results.clear();
dfs(1, 0, edges[i].first, edges[i].second);
// print
//for(auto it : results) cout << it << ' ';
//cout << endl;
if(zidianxu()) answers = results;
}
}
} else {
assert(true);
}
bool first = true;
for(auto it : answers) {
if(first) first = false;
else printf(" ");
printf("%d", it);
}
printf("\n");
return 0;
}
D2T2 填数游戏
这道题考你的思维灵敏程度,看你能不能想到打表找规律。
首先要写出最暴力的程序:暴力枚举每个点是0还是1,然后暴力枚举一条路,同时又暴力枚举出另一条路径,按照题目给的\(w(P_1) > w(P_2)\)必须\(s(P_1) \leq s(P_2)\)去判断是否合法。一旦有一个不合法,这个棋盘就不合法。
枚举路径我用的是枚举全排列的做法,给出代码吧:
bool compare()// sp1 > sp2
{
for(int i = 1; i <= n + m - 1; i++)
{
if(sp1[i] > sp2[i]) return true;
else if(sp1[i] < sp2[i]) return false;
}
return false;
}
void check()// 检验一个棋盘是否合法
{
memset(p2, 0, sizeof p2);
memset(sp2, 0, sizeof sp2);
memset(p1, 0, sizeof p1);
memset(sp1, 0, sizeof sp1);
for(int i = 1; i < n; i++) p2[i] = 'D';
for(int i = n; i <= n + m - 2; i++) p2[i] = 'R';
do
{
// mei ju p2
bool flag = true;
int x = 1, y = 1;
for(int i = 1; i <= n + m - 2; i++)// calculate s(p2) named ch2
{
sp2[i] = G[x][y] + '0';
if(p2[i] == 'R') y++;
else if(p2[i] == 'D') x++;
}
for(int i = 1; i <= n + m - 2; i++) p1[i] = p2[i];
do
{
// mei ju p1
int x = 1, y = 1;
for(int i = 1; i <= n + m - 2; i++)// calculate s(p1) named temp2
{
sp1[i] = G[x][y] + '0';
if(p1[i] == 'R') y++;
else if(p1[i] == 'D') x++;
}
if(compare())// s(p1) > s(p2) => temp2 > ch2
{
flag = false;
return;
break;
}
// ch2 >= ch1
if(!flag) break;
}
while(std::next_permutation(p1 + 1, p1 + n + m - 2 + 1));
if(!flag) continue;
}
while(std::next_permutation(p2 + 1, p2 + n + m - 2 + 1));
cnt++;
}
void dfs(int x, int y)
{
if(x == n + 1)
{
check();
return;
}
G[x][y] = 0;
if(y == m) dfs(x + 1, 1);
else dfs(x, y + 1);
G[x][y] = 1;
if(y == m) dfs(x + 1, 1);
else dfs(x, y + 1);
}
复杂度奇高,\(n,m\leq 3\)勉强能跑。
接下来看看数据点:1~4是暴力分,5~10是???
明显最可能计算的是14~16,剩下的都是不能确确实实跑出来的。不然这道题可以出到\(n,m\leq 1000000\)了。
数据点可以分为两类:
- 暴力做法。1~4与14~16
- 玄学做法。5~10 11~13 17~20都是。
又考虑到输入输出及其简单,我们是不是可以打表找规律?
果然,我们发现了一些规律:
- \(ans(n,m)=ans(m,n)\)
- 当\(ans(n,m)\)中的\(n\)不变时,若\(n \geq m\)则无规律可循,而当\(n < m\)时,\(ans(n,m) = 3 \times ans(n,m-1)\)!
按照打表找规律,配合最暴力的暴力,我们就能解决前13个点,65pts。
接下来是最难的地方,如何解决14~16数据点?只要解决了后面也简单。
这里我参考了ouuan大佬的题解,使用了两个结论:
- 每一斜行必须从左下开始是连着的1,从右上结束是连着的0,不能有01交替的情况。因为如果这样就会出现一个点右边是1下边是0,因为向右走大于向下走,此时就不满足\(s(P_1) \leq s(P_2)\)了。
- 满足条件的等价条件是:每个格子右边的格子先往下再往右的路径小于等于下面的格子先往右再往下的路径。个人感性理解就是:根据1结论,靠上边走会吃到更多的0,靠下边走会吃到更多的1,向右走大于向下走,而向右走有更多的1,向下走有更多的0,如果这种情况都成立,那么从这个点出发的所有路径都满足。
所以ouuan大佬就写了非常优美的暴力:枚举每一斜行来爆搜,然后每次check只需要走这两段路。
#include <iostream>
#include <cstdio>
using namespace std;
const int N=100;
bool check(int x,int y);
void dfs(int sum);
int n,m,a[N][N],ans;
int main()
{
cin>>n>>m;
dfs(2);
cout<<ans;
return 0;
}
void dfs(int sum)
{
int i,j;
if (sum>n+m)
{
for (i=1;i<n;++i)
{
for (j=1;j<m;++j)
{
if (!check(i,j))
{
return;
}
}
}
++ans;
return;
}
dfs(sum+1);
for (i=n;i>=1;--i) //将左下填成1
{
if (sum-i>=1&&sum-i<=m)
{
a[i][sum-i]=1;
dfs(sum+1);
}
}
for (i=n;i>=1;--i) //回溯时复原
{
if (sum-i>=1&&sum-i<=m)
{
a[i][sum-i]=0;
}
}
}
bool check(int x,int y)
{
int rx=x,ry=y+1,dx=x+1,dy=y;
while (rx+ry<=n+m)
{
if (a[rx][ry]!=a[dx][dy])
{
return a[rx][ry]<a[dx][dy];
}
if (rx<n)
{
++rx;
}
else
{
++ry;
}
if (dy<m)
{
++dy;
}
else
{
++dx;
}
}
return true;
}
最后配合上我们上面打表的结论就能解决了。
我太难了
D2T3 保卫王国
动态dp。这辈子都不可能达到这种高度了。
哎。
最后
谨以此纪念爆炸的NOIP2018
55555555