NOIP2018 复盘
前言
在这里立一个可能无法实现的flag:
把NOIP从古至今(luogu上有)的每一年都写一篇复盘!!!
伏拉格综合征开始了
在复盘就不讲那些伤心的话了。
D1T1 铺设道路
考试时居然不知道这道题是原题。。。
一共有两种做法:
递推/贪心。
设一个数组\(f\),顺序遍历。是这么更新的:
\[f[i]=f[i-1]+max(0,a[i]-a[i-1])\]
反正我没做过原题想不出来分治。
弄一个递归的函数,暴力统计区间最小值,暴力区间减,再来一个遍历找出断点,把所有的答案加起来就完事了。
但是据说这种做法是会被卡成\(O(n^2)\)的。但是幸好NOIP的数据没卡。
不然我就省四退役了
代码:(会被卡的做法2)
#include
#include
#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
#include
#include
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
#define ll long long
const int maxn = 50005;
std::vector > G[maxn];
std::multiset s[maxn];
int n, m;
// get the diameter of tree
int dep[maxn];
void bfs(int s, int &maxdep, int &idx) {
std::queue 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
using std::cin;
using std::cout;
using std::endl;
#define ll long long
#define pii pair
const int maxn = 5005;
std::vector G[maxn];
bool vis[maxn];
int n, m;
int dfn[maxn], low[maxn], dtot;
int col[maxn], ctot;
std::stack sta;
std::vector 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 填数游戏
待填。。。
D2T3 保卫王国
动态dp。这辈子都不可能达到这种高度了。
哎。
最后
谨以此纪念爆炸的NOIP2018
55555555