HNOI[2009]题解

继续上HNOI 2009的题解。

梦幻布丁

由于数据范围为100000,暴力肯定是不行的。(好像是废话…)

我们目前存在两个问题:

1.合并两个不同的颜色;
2.输出块数。

介绍一下启发式合并,通俗的说就是把小的合并到大的块上去。
合并的时间是 O(min{len1,len2}) ,而有效合并的次数不超过 O(logn) ,所以时间复杂度为 O(nlogn)

而我们只需要在合并的时候(假设 ab )判断该 a 颜色的两边是否有b颜色,若有答案相减即可。

其实我们还漏了一个小问题,万一我们需要将 a 颜色变为b颜色,而 a 颜色却大于b颜色,难道我们要将大的合并至小的上吗?那样时间复杂度就不合要求了。

其实我们只需要令 f[x] 表示颜色 x 在块中的颜色,需要时交换f[x]即可,这样我们就依然能保证从小的合并到大的上。

3.set水过版

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int maxn = 100009;
const int maxc = 1000009;

int n, m;
int a[maxn];
int f[maxc];
set<int> C[maxc];
int ans;

void solve(int u, int v)
{
    for(set<int>::iterator it = C[u].begin(); it != C[u].end(); ++it)
    {
        if(a[*it-1] == v) ans--;
        if(a[*it+1] == v) ans--;
        C[v].insert(*it);
    }
    for(set<int>::iterator it = C[u].begin(); it != C[u].end(); ++it)
        a[*it] = v;
    C[u].clear();
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i < maxc; ++i) f[i] = i;
    for(int i = 1; i <= n; ++i)
    {
        scanf("%d", a+i);
        if(a[i] != a[i-1]) ans++;
        C[a[i]].insert(i);  
    }
    for(int i = 1; i <= m; ++i)
    {
        int T;scanf("%d", &T);
        if(T == 2) printf("%d\n", ans);
        else
        {
            int u, v;
            scanf("%d%d", &u, &v);
            if(u == v) continue;
            if(C[f[u]].size() > C[f[v]].size()) swap(f[u], f[v]);
            solve(f[u], f[v]);
        }
    }
    return 0;
}

4.链表版

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int maxn = 100009;
const int maxc = 1000009;

int n, m;
int a[maxn];
int f[maxc];
list<int> C[maxc];
int ans;

void solve(int u, int v)
{ 
    for(list<int>::iterator i = C[u].begin(); i != C[u].end(); ++i)
    {
        if(a[*i-1] == v) ans--;
        if(a[*i+1] == v) ans--;
        C[v].push_back(*i);
    }
    for(list<int>::iterator i = C[u].begin(); i != C[u].end(); ++i)
        a[*i] = v;
    C[u].clear();
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i < maxc; ++i) f[i] = i;
    for(int i = 1; i <= n; ++i)
    {
        scanf("%d", a+i);
        if(a[i] != a[i-1]) ans++;
        C[a[i]].push_back(i);
    }
    for(int i = 1; i <= m; ++i)
    {
        int T;scanf("%d", &T);
        if(T == 2) printf("%d\n", ans);
        else
        {
            int u, v;
            scanf("%d%d", &u, &v);
            if(u == v) continue;
            if(C[f[u]].size() > C[f[v]].size()) swap(f[u], f[v]);
            solve(f[u], f[v]);
        }
    }
    return 0;
}

图的同构

很明显是Pólya计数,但怎样推理才是最重要的。

首先,我们来回忆一下Pólya计数的公式:
                                     L=1|G|mc(gi)

其中 L 为等价类个数,|G|为置换个数, m 为颜色种数,c(gi)表示第 i 个置换的不动点数(也就是循环个数)。

根据题意,点置换有N!个,很明显对于 N=60 的数据是不可能一一计算循环个数的,但我们可以发现有很多种置换的循环是等价的,也就是说我们可以只求出其中很小一部分的答案就可以了。

我们考虑将点置换映射到边置换上(==,点置换无法计算颜色啊,边置换可以用两种颜色表示有还是没有这条边),每个点置换是唯一对应一个边置换的。
即点置换: ip[i] ,边置换: (i,j)(p[i],p[j])

考虑对于 n ,我们将其划分为m段,且有:
                       L1L2L3 ...Lm>0 ,n=Li
将每一段视为一个循环,我们考虑在此种点置换下边置换的循环个数。

考虑一条边 (i,j)
1. i,j 在同一循环 L 内,则此种边构成的循环个数为[L2]个。
证明:

   1o L 为奇数时,我们考虑一个循环节(1,2,3,...,2n+1)L=2n+1,边置换为                        [(1,2)(2,3)(1,3)(2,4)(1,4)(2,5)......(2n,2n+1)(2n+1,1)]
对于以边 (1,k),kn 开头的循环,我们可以经过 2n+1 步走过一个循环,又一共有 n(2n+1) 个置换,故有 n=[L2] 个循环。

   2o L 为偶数时,类似的当kL21的时候与上面类似,只是当 k=L2 我们只需经 L2 步即可走完循环。
    综上,证毕。

2. i,j 在两循环 L1,L2 内,则此种边构成的循环个数为 gcd(L1,L2) 个。
证明:
    两循环间共有 L1L2 条边,而类似上面证明的我们考虑只有走了 lcm(L1,L2) 步我们才能回到原点,因此共有 L1L2lcm(L1,L2)=gcd(L1,L2) 个循环。

有了以上结论,我们可以得出对于 n 的一个划分的循环个数为关于L1,L2,...,Lm的函数。

接下来我们考虑有多少种点置换对应了这种划分。

考虑将 N 个点插入有N!种方法,而每个循环内重复计算了 Li 次,并且当存在 Li=Li+1=...=Lj 的时候,它们的顺序时刻任意调换的,故总的个数为:
                                          N!L1L2...Lmk1k2...kt
其中 t 表示有t种不同的 L 值,每种值有ki个。

我们只需把每种划分对应的置换个数 2c 统计入答案,其中 c 为边置换的循环节数,即可。

至于每种划分我们dfs即可。

对于这种置换总数非常大的题目,我们可以从不同的循环下手,计算每种循环对应了多少置换来简化计算。

AC code:

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <climits>
#include <cmath>
#include <string>
#include <iostream>
#include <algorithm>
#include <set>
#include <map>
#include <list>
#include <queue>
#include <stack>
typedef long long LL;
typedef long double LD;
typedef double DB;
using namespace std;

const int MaxN = 69;
const int Mod = 997;

int n;
int inv[MaxN];
int L[MaxN];
int ans;
int Gcd[MaxN][MaxN];
int invl[MaxN];
int mi[MaxN*MaxN];

int PowerMod(int a, int b, int m)
{
    int re = 1, base = a;
    while(b)
    {
        if(b&1) re = re*base%m;
        base = base*base%m;
        b >>= 1;
    }
    return re;
}

#define Inv(a, b) (PowerMod(a, b-2, b))

int gcd(int a, int b)
{
    if(b) return gcd(b, a%b);
    else return a;
}

int calc(int m)
{
    int re = 1;
    for(int i = 1, l = 1; i <= m; ++i)
    {
        re = re*invl[L[i]]%Mod;
        if(i == m || L[i] != L[i+1])
        {
            re = re*inv[l]%Mod;
            l = 1;
        }
        else l++;
    }
    int num = 0;
    for(int i = 1; i <= m; ++i)
    {
        num += L[i]/2;
        for(int j = i+1; j <= m; ++j)
            num += Gcd[L[i]][L[j]];
    }
    re = re*mi[num]%Mod;
    return re;
}

void Dfs(int cnt, int sum)
{
    L[cnt] = n-sum;
    ans = (ans+calc(cnt))%Mod;
    for(int i = L[cnt-1]; i*2 <= n-sum; ++i)
    {
        L[cnt] = i;
        Dfs(cnt+1, sum+i);
    }
}

int main()
{
    cin >> n;
    inv[0] = 1;
    for(int i = 1; i <= n; ++i)
    {
        inv[i] = inv[i-1]*Inv(i, Mod)%Mod;
        invl[i] = Inv(i, Mod);
    }
    mi[0] = 1;
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= n; ++j)
            Gcd[i][j] = gcd(i, j), mi[(i-1)*n+j] = mi[(i-1)*n+j-1]*2%Mod;
    L[0] = 1;
    Dfs(1, 0);
    cout << ans << endl;
    return 0;
}

双递增序

f[i][j]表示第 i 个元素所在的序列有j个元素时,另一个序列最后一个元素的最小值, dp 方程为:
                   f[i][j]=min{f[i1][j1](s[i]>s[i1]),                                s[i1](s[i]>f[i1][ij])}

AC code:

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <climits>
#include <cmath>
#include <string>
#include <iostream>
#include <algorithm>
#include <set>
#include <map>
#include <list>
#include <queue>
#include <stack>
typedef long long LL;
typedef long double LD;
typedef double DB;
using namespace std;

const int MaxN = 2009;
const int inf = 0x3f3f3f3f;
int n, s[MaxN];
int f[MaxN][MaxN<<1];

int main()
{
    int Test;scanf("%d", &Test);
    while(Test--)
    {
        scanf("%d", &n);
        for(int i = 1; i <= n; ++i)
            scanf("%d", s+i);
        memset(f, inf, sizeof(f));
        f[0][0] = 0;
        for(int i = 0; i < n; ++i)
            for(int j = 0; j <= (n>>1) && j <= i; ++j)
                if(f[i][j] < inf)
                {
                    if(s[i+1] > s[i]) f[i+1][j+1] = min(f[i+1][j+1], f[i][j]);
                    if(s[i+1] > f[i][j]) f[i+1][i-j+1] = min(f[i+1][i-j+1], s[i]);
                }
        if(f[n][n>>1] < inf) puts("Yes!");
        else puts("No!");
    }
    return 0;
}

通往城堡之路

题解传送门(其实是我个地方没看懂= =):
http://blog.csdn.net/ts124124/article/details/6249475

AC code:

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int maxn = 5009;
const LL inf = 1LL<<62;

int n;
LL d;
LL h[maxn];
LL b[maxn];
LL ans;

int main()
{
    int TAT;
    scanf("%d", &TAT);
    while(TAT--)
    {
        scanf("%d%lld", &n, &d);
        for(int i = 1; i <= n; ++i)
            scanf("%lld", h+i);
        if(h[1]-(n-1)*d > h[n] || h[1]+(n-1)*d < h[n]) puts("impossible");
        else
        {
            b[1] = h[1];
            for(int i = 2; i <= n; ++i)         
                b[i] = b[i-1]-d;
            while(h[n] != b[n])
            {
                int now = 0, j = 0; 
                LL up = inf, Max = -inf, val = 0;
                for(int i = n; i >= 2; --i)
                {
                    if(b[i] >= h[i]) now--;
                    else now++, up = min(up, h[i]-b[i]);
                    if(now > Max && b[i] < b[i-1]+d)
                    {
                        Max = now;
                        val = min(up, b[i-1]+d-b[i]);
                        j = i;
                    }
                }
                for(int i = j; i <= n; ++i)
                    b[i] += val;
            }
            ans = 0;
            for(int i = 1; i <= n; ++i)
                ans += abs(b[i]-h[i]);
            printf("%lld\n", ans);
        }
    }
    return 0;
}

无归岛

比较水的仙人掌 dp ,因为只有题目中只有环,所以搜到环时,直接做两遍 dp f[]g[] 分别表示取或不取这个点,做两遍 dp ,注意初始化即可。

AC code:

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int MaxN = 100009;
const int MaxM = 200009;
const int inf = 2e9+5;

int n, m;
struct UGraph
{
    int size;
    int head[MaxN];
    int to[MaxM<<1];
    int ne[MaxM<<1];
    UGraph(){size = 1;}
    inline void AddEdge(int u, int v)
    {
        to[size] = v, ne[size] = head[u], head[u] = size++; 
        to[size] = u, ne[size] = head[v], head[v] = size++;
    }
}G;
int _Time_;
int dfn[MaxN];
int f[MaxN], g[MaxN];
int fa[MaxN];

int u0, v0;
int u1, v1;

void update(int start, int end)
{
    u0 = v0 = 0;
    for(int i = end; i != start; i = fa[i])
    {
        u1 = f[i]+v0;
        v1 = g[i]+max(u0, v0);
        u0 = u1, v0 = v1;
    }
    g[start] += max(u0, v0);
    v0 = -inf, u0 = 0;
    for(int i = end; i != start; i = fa[i])
    {
        u1 = f[i]+v0;
        v1 = g[i]+max(u0, v0);
        u0 = u1, v0 = v1;
    }
    f[start] += v0;
}

void dfs(int cnt, int father)
{
    dfn[cnt] = ++_Time_;
    fa[cnt] = father;
    for(int i = G.head[cnt]; i; i = G.ne[i])
        if(!dfn[G.to[i]])
            dfs(G.to[i], cnt);
    for(int i = G.head[cnt]; i; i = G.ne[i])
        if(fa[G.to[i]] != cnt && dfn[G.to[i]] > dfn[cnt])
            update(cnt, G.to[i]);
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= m; ++i)
    {
        static int u, v;
        scanf("%d%d", &u, &v);
        G.AddEdge(u, v);    
    }
    for(int i = 1; i <= n; ++i)
        scanf("%d", f+i);
    dfs(1, 0);
    printf("%d\n", max(f[1], g[1]));
    return 0;
}

有趣的数列

打表找规律后我们可以发现是 Catalan 数,然而由于模数不是质数,且一个一个数的质因数分解会超时,我们可以用欧拉筛法在筛素数的过程中记录其最小质因子,在乘法时对该质因子++,除法时- -,直接分解即可。

AC code:

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int MaxN = 1000009;

int n, p;
int prime[MaxN<<1], tot;
int Min[MaxN<<1];
int s[MaxN<<1];
LL ans = 1;

void Euler(int n)
{
    static int hash[MaxN<<1] = {0};
    for(int i = 2; i <= n; ++i)
    {
        if(!hash[i]) prime[++tot] = i, Min[i] = tot;
        for(int j = 1; j <= tot && prime[j]*i <= n; ++j)
        {
            hash[prime[j]*i] = 1;Min[prime[j]*i] = j;
            if(i%prime[j] == 0) break;  
        }
    }
}

void Add(int x, int f)
{
    while(x != 1)
    {
        s[Min[x]] += f;
        x /= prime[Min[x]];
    }
}

int main()
{
    cin >> n >> p;
    Euler(2*n);
    for(int i = 2*n; i > n; --i) Add(i, 1);
    for(int i = n; i >= 2; --i) Add(i, -1);
    Add(n+1, -1);
    for(int i = 1; i <= tot; ++i)
        while(s[i]--) ans = (ans*prime[i])%p;
    cout << ans << endl;
    return 0;
}

最小圈

二分答案,然后 spfa 判负环即可,有个神奇的地方是我们要用 dfs spfa 才会不超时。

AC code:

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <list>
#include <stack>
#include <queue>
typedef long long LL;
typedef double DB;
typedef long double LD;
using namespace std;

const int MaxN = 3009;
const int MaxM = 10009;
const double inf = 1e8;
const double eps = 1e-9;

int n, m;
struct DGraph
{
    int Es;
    int head[MaxN];
    int to[MaxM];
    int ne[MaxM];
    DB d[MaxM];
    DGraph()
    {
        Es = 1;
        memset(head, 0, sizeof(head));  
    }
    inline void AddEdge(int u, int v, DB w)
    {
        to[Es] = v;d[Es] = w;ne[Es] = head[u];head[u] = Es++;   
    }
    inline void AddVal(DB k)
    {
        for(int i = 1; i < Es; ++i)
            d[i] += k;  
    }
}G;
bool InStack[MaxN];
DB dis[MaxN];

bool spfa(int u)
{
    bool flag = false;
    InStack[u] = true;
    for(int i = G.head[u]; i; i = G.ne[i])
    {
        if(dis[G.to[i]] > G.d[i]+dis[u])
        {
            if(InStack[G.to[i]]) {flag = true;break;}
            else
            {
                dis[G.to[i]] = dis[u]+G.d[i];
                if(spfa(G.to[i])) {flag = true;break;}  
            }
        }
    }
    InStack[u] = false;
    return flag;
}

bool check(DB mid)
{
    bool flag = false;
    G.AddVal(-mid);
    memset(dis+1, 0.0, n*sizeof(DB));
    for(int i = 1; i <= n; ++i)
        if(spfa(i)) {flag = true;break;}
    G.AddVal(mid);
    return flag;
}

int main()
{
    scanf("%d%d", &n, &m);
    DB l = inf, r = 0;
    for(int i = 1; i <= m; ++i)
    {
        int u, v, d;
        scanf("%d%d%d", &u, &v, &d);
        G.AddEdge(u, v, d*1.0); 
        l = min(l, d*1.0);r = max(r, d*1.0);
    }
    while(r-l > eps)
    {
        DB mid = (l+r)*0.5;
        if(check(mid)) r = mid;
        else l = mid;
    }
    printf("%.8lf\n", l);
    return 0;
}

积木 Block

不会做…先晾在这吧…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值