NOIP2017 复盘

NOIP2017 复盘

D1T1 小凯的疑惑

可惜了,我当时只会写\(1\leq a,b \leq 50\)的暴力。我还以为这道题就这样了。

首先,看到这道题目的输入输出这么少,而且\(a,b\leq 10^9\),就应该想到打表找规律!

暴力程序真的不要太好写,但是规律就难看出来了。

结论是\(a \times b - a - b\)

合理利用diff或者fc就很好对拍了。

D1T2 时间复杂度

大模拟,从字符串读入开始就不容易写

首先,永远不可能用单个char去读入,至少要用char数组,最方便是用std::string。当年我就是在这个地方爆零的。

然后这种模拟循环嵌套的时候,很自然就会想到系统栈,我们用栈可以模拟一下。

稍微动点脑的部分是如何算时间复杂度。小学生都知道就是在一个循环中找到最大的复杂度,然后加上自身的(当然除去不执行部分的)。

分类讨论一下可以简单地算出每个循环本身的时间复杂度:

  1. x是数字,y也是数字:\(x \leq y\)\(O(1)\)\(x>y\)算不执行。
  2. x是数字,y是\(n\)\(O(n)\)
  3. x是\(n\),y是数字:不执行。
  4. x和y都是\(n\)\(O(1)\)

如果程序语法不错误,那么只需要按上面的做法算就可以了。没错,70pts!

然后问题来了:如何解决语法错误部分。

考虑一下语法错误可能又什么引起:

  1. 变量名重复
  2. 循环有开始没结尾
  3. 循环有结尾没开始

第一个问题我们使用一个vis数组就能解决,我们同时以变量名为下标来储存它们的本身时间复杂度和嵌套时间复杂度,这样解决不容易错。

第二个问题,只需要在最后看看栈是否为空即可。

第三个问题,想想你也不会让栈下溢吧。

所以就能够解决这道大模拟了。

/*************************************************************************
 @Author: Garen
 @Created Time : Sun 17 Mar 2019 08:44:12 AM CST
 @File Name: P3952.cpp
 @Description:
 ************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::stringstream;
const int maxn = 105;
string str[maxn];
bool vis[30];
int complexity[30], new_complexity[30];
int l;

int trans(string str) {
    int res = 0;
    for(int i = 0; str[i]; i++) {
        res = res * 10 + str[i] - '0';
    }
    return res;
}
int cal(string x, string y) {
    if(x[0] == 'n') {
        if(y[0] == 'n') {
            return 0;
        } else {
            return -1;
        }
    } else {
        if(y[0] == 'n') {
            return 1;
        } else {
            int xx = trans(x), yy = trans(y);
            if(xx <= yy) return 0;
            else return -1;
        }
    }
}
int solve(int comp) {
    memset(vis, false, sizeof vis);
    std::stack<int> sta;
    int ans = 0;
    bool ignore = false;
    for(int i = 1; i <= l; i++) {
        stringstream ss(str[i]);
        char opt; ss >> opt;
        if(opt == 'F') {
            char name; string x, y; ss >> name >> x >> y;
            if(vis[name - 'a' + 1]) return 3;
            vis[name - 'a' + 1] = true;
            sta.push(name - 'a' + 1);
            if(!ignore) {
                int new_comp = cal(x, y);
                //cout << "new_comp: " << new_comp << endl;
                complexity[name - 'a' + 1] = new_complexity[name - 'a' + 1] = new_comp;
                if(complexity[name - 'a' + 1] == -1) ignore = true;
            }
        } else if(opt == 'E') {
            if(sta.empty()) return 3;
            int x = sta.top(); sta.pop(); vis[x] = false;
            if(ignore) {
                if(!sta.empty() && complexity[sta.top()] != -1) ignore = false;
                continue;
            }
            if(complexity[x] == -1) {
                
            } else {
                if(sta.empty()) ans = std::max(ans, new_complexity[x]);
                else new_complexity[sta.top()] = std::max(new_complexity[sta.top()], complexity[sta.top()] + new_complexity[x]);
            }
            complexity[x] = new_complexity[x] = 0;
        }
    }
    if(!sta.empty()) return 3;
    if(ans == comp) return 1;
    else return 2;
}
int main() {
    int t; cin >> t;
    while(t--) {
        string guess;
        cin >> l >> guess;
        for(int i = 0; i <= l; i++) getline(cin, str[i]);
        int comp = -1;
        if(guess[2] == '1') {
            comp = 0;
        } else {
            stringstream ss(guess.substr(4));
            ss >> comp;
        }
        //cout << "comp: " << comp << endl;
        int ans = solve(comp);
        if(ans == 1) puts("Yes");
        else if(ans == 2) puts("No");
        else if(ans == 3) puts("ERR");
        else puts("GG");
    }
    return 0;
}

D1T3 逛公园

2019.03.16 把记忆化搜索的做法抄了下来

30pts做法

\((K=0)\)就是经典的最短路计数。在最短路算法的基础上算一下就行了。

70pts做法

发现\(K \leq 50\),于是很显然能发现这道题是与\(K\)有关的dp。

我们设一下dp状态:\(dp[u][l]\)代表从\(1\)点走到\(u\)点,满足\(dist \leq dist_u + l\)的方案数。最终答案就是\(\sum_{l=0}^K dp[n][l]\)

要算某个状态对应的\(dist\)范围,显然我们需要从\(1\)点跑个最短路作为预处理。

如何求这个dp?我们可以用记忆化搜索。

在记忆化搜索中,我们翻回前面的边,看看能从前面的哪个状态转移得来。

如何翻回前面的边?建个反图即可。

那前面的状态中,\(l\)是什么?是\(l - (dist_v - dist_u + w)\)。(画个图很好理解,括号里面的是会走多的冤枉路)

按照这种思路去记忆化搜索就应该能拿70pts了吧。。。

100pts做法

知道为什么上面只有70pts吗?因为我们忘记考虑无解情况。

怎么样才能出现无穷多条路径?当走了0环的时候。

做法只需要在前面的基础上修改一点点:

  • 给每个状态记录一个\(instack\),如果碰到了就说明有0环。
  • 只要在自己枚举的所有\(l\)出现了一个0环,那么就无解。
  • 利用记忆化搜索的返回值很容易实现。

因为从\(1\)走到\(n\)跟从\(n\)走到\(1\)是等价的,所以你可以把上面的两个图颠倒顺序,答案是不变的。

看起来好像不是太难啊但是永远是独立写不出来的

代码:

/*************************************************************************
 @Author: Garen
 @Created Time : Sat 16 Mar 2019 01:22:44 PM CST
 @File Name: P3953.cpp
 @Description:
 ************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
#define ll long long
const int maxn = 100005;
std::vector<std::pair<int,int> > G[maxn], GG[maxn];
int dist[maxn];
bool vis[maxn];
ll dp[maxn][51];
bool instack[maxn][51];
int n, m, k; ll p;

int read() {
    int ans = 0, s = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0') {
        if(ch == '-') s = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9') {
        ans = ans * 10 + ch - '0';
        ch = getchar();
    }
    return s * ans;
}
void spfa(int s) {
    std::queue<int> q;
    memset(dist, 0x3f, sizeof dist);
    dist[s] = 0; q.push(s); vis[s] = true;
    while(!q.empty()) {
        int u = q.front(); q.pop(); vis[u] = false;
        for(auto it: GG[u]) {
            int v = it.first, w = it.second;
            if(dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if(!vis[v]) {
                    q.push(v); vis[v] = true;
                }
            }
        }
    }
}
ll solve(int u, int l) {
    if(l < 0 || l > k) return 0;
    if(instack[u][l]) {
        instack[u][l] = false;
        return -1;
    }
    if(dp[u][l] != -1) return dp[u][l];
    instack[u][l] = true;
    ll ans = 0;
    for(auto it: G[u]) {
        int v = it.first, w = it.second;
        ll temp = solve(v, l - (dist[v] - dist[u] + w));
        if(temp == -1) {
            instack[u][l] = false;
            return -1;
        }
        ans += temp, ans %= p;
    }
    if(u == n && l == 0) ans++;
    instack[u][l] = false;
    return dp[u][l] = ans;
}
int main() {
    int T = read();
    while(T--) {
        for(int i = 1; i <= n; i++) G[i].clear(), GG[i].clear();
        n = read(), m = read(), k = read(), p = read();
        while(m--) {
            int u = read(), v = read(), w = read();
            G[u].push_back(std::make_pair(v, w));
            GG[v].push_back(std::make_pair(u, w));
            
        }
        spfa(n);
        memset(dp, -1, sizeof dp);
        ll ans = 0;
        for(int i = 0; i <= k; i++) {
            ll temp = solve(1, i);
            if(temp == -1) {
                ans = -1; break;
            }
            ans += temp, ans %= p;
        }
        if(ans == -1) cout << -1 << endl;
        else cout << ans << endl;
    }
    return 0;
}

D2T1 奶酪

最简单的做法:直接连边跑bfs就可以了。

#include<cstdio>
#include<cmath>
#include<cstring>
const int maxn = 1005;
struct Circle
{
	int x, y, z;
} s[maxn];
struct Edges
{
	int next, to;
} e[2000005];
int head[maxn], tot;
int n, h, r;
int start, end;
bool vis[maxn];
int queue[100005], front, rear;
void link(int u, int v)
{
	e[++tot] = (Edges){head[u], v};
	head[u] = tot;
}
double dist(double x, double y, double z, double xx, double yy, double zz)
{
	return sqrt((x - xx) * (x - xx) + (y - yy) * (y - yy) + (z - zz) * (z - zz));
}
bool bfs()
{
	memset(vis, false, sizeof vis);
	front = rear = 0;
	queue[rear++] = start;
	vis[start] = true;
	while(front < rear)
	{
		int u = queue[front++];
		if(u == end)
		{
			return true;
		}
		for(int i = head[u]; i; i = e[i].next)
		{
			int v = e[i].to;
			if(!vis[v])
			{
				queue[rear++] = v;
				vis[v] = true;
			}
		}
	}
	return false;
}
int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		scanf("%d%d%d", &n, &h, &r);
		for(int i = 1; i <= n; i++) scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].z);
		memset(head, 0, sizeof head); tot = 0;
		start = n + 1; end = start + 1;
		for(int i = 1; i <= n; i++)
		{
			if(s[i].z <= r) link(start, i);
			if(s[i].z + r >= h) link(i, end);
			for(int j = i + 1; j <= n; j++)
			{
				if(dist(s[i].x, s[i].y, s[i].z, s[j].x, s[j].y, s[j].z) <= 2 * r)
				{
					link(i, j); link(j, i);
				}
			}
		}
		if(bfs()) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

其实并查集可以做,常数特别小。

#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 1005;
struct hole
{
	long long x, y, z;
} holes[maxn];
int fa[maxn];
int n, h, r;
double dist(hole a, hole b)
{
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z));
}
void clear()
{
	for(int i = 1; i < maxn; i++)
	{
		fa[i] = i;
		holes[i].x = holes[i].y = holes[i].z = 0;
	}
}
int find(int x)
{
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
	x = find(x); y = find(y);
	if(x != y)
	{
		if(holes[x].z > holes[y].z) fa[y] = x;
		else fa[x] = y;
	}
}
bool check()
{
	for(int i = 1; i <= n; i++)
	{
		if(holes[i].z <= r)
		{
			if(holes[find(i)].z + r >= h) return true;
		}
	}
	return false;
}
int main()
{
	int T; scanf("%d", &T);
	while(T--)
	{
		clear();
		scanf("%d%d%d", &n, &h, &r);
		for(int i = 1; i <= n; i++)
		{
			scanf("%lld%lld%lld", &holes[i].x, &holes[i].y, &holes[i].z);
			for(int j = 1; j < i; j++)
			{
				if(dist(holes[i], holes[j]) <= 2 * r)
					merge(i, j);
			}
		}
		if(check()) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

D2T2 宝藏

这里是两种方法:第一种是随机算法,第二种是状压dp。

随机算法的思想是获得一个开辟的序列,按照这个序列每次贪心地延伸,得到的就是该序列最小的代价。

再套上模拟退火的模板,随便调一调参数,卡时就能过了。

注意:exp(DE / T) * RAND_MAX > rand()不要写错了。。。

代码:

/*************************************************************************
 @Author: Garen
 @Created Time : Sun 17 Mar 2019 02:36:20 PM CST
 @File Name: P3959.cpp
 @Description:
 ************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
#define ll long long
const int maxn = 20;
const int INF = 0x3f3f3f3f;
int G[maxn][maxn];
int order[maxn], dep[maxn];
int n, m;
ll ans;

ll getans() {
    memset(dep, 0, sizeof dep);
    dep[order[1]] = 1;
    ll res = 0;
    for(int i = 2; i <= n; i++) {
        ll temp = 0x3f3f3f3f3f3f3f3f; int idx = -1;
        for(int j = 1; j < i; j++) {
            if(G[order[j]][order[i]] != INF && temp > dep[order[j]] * G[order[j]][order[i]]) {
                temp = 1ll * dep[order[j]] * G[order[j]][order[i]]; idx = order[j];
            }
        }
        if(temp != 0x3f3f3f3f3f3f3f3f) {
            dep[order[i]] = dep[idx] + 1; res += temp;
        } else return 0x3f3f3f3f3f3f3f3f;
    }
    return res;
}
void init() {
    for(int i = 1; i <= n; i++) order[i] = i;
    ans = getans();
}
void SA() {
    for(double T = 12345; T > 1e-6; T *= 0.99) {
        int x = rand() % n + 1, y = rand() % n + 1;
        while(x == y) x = rand() % n + 1, y = rand() % n + 1;// 1个点就卡死了
        std::swap(order[x], order[y]);
        ll newans = getans();
        ll DE = newans - ans;
        if(DE < 0) ans = newans;
        else if(exp(-DE / T) * RAND_MAX > rand()) ;
        else std::swap(order[x], order[y]);
    }
}
int main() {
    std::ios::sync_with_stdio(false);
    srand(19260817);
    cin >> n >> m;
    if(n == 1) {
        cout << 0 << endl;
        return 0;
    }
    memset(G, 0x3f, sizeof G);
    for(int i = 1; i <= n; i++) G[i][i] = 0;
    while(m--) {
        int u, v, w; cin >> u >> v >> w;
        G[u][v] = std::min(G[u][v], w);
        G[v][u] = std::min(G[v][u], w);
    }
    init();
    for(int i = 1; i <= 50; i++) SA();
    cout << ans << endl;
    return 0;
}

另一种是状压dp。

dp状态好设:dp[i][j]为开辟到第\(i\)层,开辟状态为\(j\)的最小代价。

方程就可以写成:\(dp[i + 1][j | k] = min(dp[i][j] + i \times trans[j][k])\)

更新dp的方法很简单:枚举起点\(i\),枚举当前状态\(S\),枚举\(S\)的补集的子集\(j\),更新开辟\(j\)代表的所有点后的最小代价。

枚举子集方法是for(int j = S; j; j = (j - 1) & S)

但是问题来了:如何快速地得到任意两个状态之间转换的最小长度。

我们只要预处理就可以:枚举初始状态\(i\),枚举\(i\)的补集的子集\(j\),再枚举子集\(j\)中的点\(k\),考虑从该点开辟出\(i\)中的所有点\(l\),找到最小长度,依次加入答案即可。

初始状态\(dp[1][2^{i-1}] = 0\), 答案就是\(\max(dp[i][S])\)

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
const int maxn = 13, maxN = 4105;
const int INF = 0x3f3f3f3f;
int G[maxn][maxn];
int n, m;
int dp[maxn][maxN];
int trans[maxN][maxN];
int main()  
{
    scanf("%d%d", &n, &m);
    memset(G, 0x3f, sizeof G);
    memset(dp, 0x3f, sizeof dp);
    while(m--)
    {
        int u, v, w; scanf("%d%d%d", &u, &v, &w);
        G[u][v] = G[v][u] = std::min(G[u][v], w);
    }
    for(int i = 1; i <= n; i++) dp[1][1 << (i - 1)] = 0;
    int S = 1 << n;

    for(int i = 1; i < S; i++)
    {
        int s = S - 1 - i;
        for(int j = s; j; j = (j - 1) & s)
        {
            for(int k = 1; k <= n; k++)
            {
                if((1 << (k - 1)) & j)
                {
                    int temp = INF;
                    for(int l = 1; l <= n; l++)
                    {
                        if((1 << (l - 1)) & i)
                        {
                            temp = std::min(temp, G[l][k]);
                        }
                    }
                    if(temp == INF)
                    {
                        trans[i][j] = INF;
                        break;
                    }
                    else trans[i][j] += temp;
                }
            }
        }
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j < S; j++)
        {
            int s = S - 1 - j;
            for(int k = s; k; k = (k - 1) & s)
            {
                if(trans[j][k] == INF) continue;
                dp[i + 1][j | k] = std::min(dp[i + 1][j | k], dp[i][j] + i * trans[j][k]);
            }
        }
    }
    int ans = INF;
    for(int i = 1; i <= n; i++) ans = std::min(ans, dp[i][S - 1]);
    printf("%d\n", ans);
    return 0;
}

D2T3 列队

这道题可以用平衡树来做。

把每一行除了最后一个元素拿起来建一颗平衡树,再把最后一列独立建一颗平衡树。

其实思路很简单,但是代码就不是那么好写了。。。

// luogu-judger-enable-o2
#include<cstdio>
#define ll long long
const ll maxn = 3000005;
ll n, m, q;
ll size[maxn], fa[maxn], ch[maxn][2];
ll l[maxn], r[maxn];
ll tot;
struct Splay
{
    ll root;
    ll dir(ll x)
    {
        return ch[fa[x]][1] == x;
    }
    void connect(ll son, ll f, ll k)
    {
        fa[son] = f;
        ch[f][k] = son;
    }
    void pushup(ll x)
    {
        size[x] = size[ch[x][0]] + size[ch[x][1]] + r[x] - l[x] + 1;
    }
    void rotate(ll x)
    {
        ll y = fa[x];
        ll z = fa[y];
        ll yk = dir(x);
        ll zk = dir(y);
        ll b = ch[x][yk ^ 1];
        connect(b, y, yk);
        connect(y, x, yk ^ 1);
        connect(x, z, zk);
        pushup(y);
        pushup(x);
    }
    void splay(ll x, ll goal)
    {
        while(fa[x] != goal)
        {
            ll y = fa[x];
            ll z = fa[y];
            if(z != goal) dir(x) == dir(y) ? rotate(y) : rotate(x);
            rotate(x);
        }
        if(goal == 0) root = x;
    }
    void insert(ll lx, ll rx)
    {
        ll now = root, f = 0;
        while(now)
        {
            f = now; now = ch[now][1];
        }
        now = ++tot;
        l[now] = lx; r[now] = rx; size[now] = rx - lx + 1;
        fa[now] = f; if(f) ch[f][1] = now;
        ch[now][0] = ch[now][1] = 0;
        splay(now, 0);
    }
    ll splitnode(ll x, ll k)// source[l, l + k - 1], new[l + k, r]
    {
        ll y = ++tot;
        l[y] = l[x] + k, r[y] = r[x];
        r[x] = l[x] + k - 1;
        if(ch[x][1] == 0)
        {
            ch[x][1] = y; fa[y] = x;
            splay(y, 0);
        }
        else
        {
            ll t = ch[x][1];
            while(ch[t][0]) t = ch[t][0];
            ch[t][0] = y; fa[y] = t;
            splay(y, 0);
        }
        return y;
    }
    ll popkth(ll k)
    {
        ll now = root;
        while(2333)
        {
            if(size[ch[now][0]] + r[now] - l[now] + 1 < k)
            {
                k -= size[ch[now][0]] + r[now] - l[now] + 1;
                now = ch[now][1];
            }
            else if(size[ch[now][0]] >= k) now = ch[now][0];
            else
            {
                k -= size[ch[now][0]];
                if(k != r[now] - l[now] + 1) splitnode(now, k);
                if(k != 1) now = splitnode(now, k - 1);
                break;
            }
        }
        splay(now, 0);
        fa[ch[now][0]] = fa[ch[now][1]] = 0;
        if(ch[now][0] == 0)
        {
            root = ch[now][1];
        }
        else
        {
            ll t = ch[now][0];
            while(ch[t][1]) t = ch[t][1];
            splay(t, 0);
            ch[t][1] = ch[now][1];
            fa[ch[now][1]] = t;
            pushup(root = t);
        }
        return l[now];
    }
    void print(int u)
    {
        if(ch[u][0]) print(ch[u][0]);
        printf("[%lld, %lld] ", l[u], r[u]);
        if(ch[u][1]) print(ch[u][1]);
    }
} sp[300000];
ll read()
{
    ll ans = 0, s = 1;
    char ch =getchar();
    while(ch > '9' || ch < '0'){ if(ch == '-') s = -1; ch = getchar(); }
    while(ch >= '0' && ch <= '9') ans = ans * 10 + ch - '0', ch = getchar();
    return s * ans;
}
int main()
{
    n = read(), m = read(), q = read();
    for(ll i = 1; i <= n; i++)
    {
        sp[i].insert((i - 1) * m + 1, i * m - 1);
        sp[0].insert(i * m, i * m);
    }
    while(q--)
    {
        ll x = read(), y = read();
        if(y == m)
        {
            ll temp1 = sp[0].popkth(x);
            sp[0].insert(temp1, temp1);
            printf("%lld\n", temp1);
        }
        else
        {
            ll temp1 = sp[x].popkth(y);
            ll temp2 = sp[0].popkth(x);
            sp[x].insert(temp2, temp2);
            sp[0].insert(temp1, temp1);
            printf("%lld\n", temp1);
        }
        /*
        for(ll i = 0; i <= n; i++)
        {
            sp[i].print(sp[i].root);
            printf("\n");
        }
        */
    }
    return 0;
}
posted @ 2019-03-16 23:53  Garen-Wang  阅读(179)  评论(0编辑  收藏  举报