音乐主题问题

本文介绍了一种算法来解决音乐旋律中找出重复且不重叠的主题子序列问题,通过后缀数组和二分查找技术,计算给定旋律中最长主题的长度。涉及的数据结构包括名数组、秩数组和高度数组,以及如何应用这些工具来找到满足条件的主题子序列。
摘要由CSDN通过智能技术生成

一 问题描述

音乐旋律表示为 N(1<=N<=20000)个音符的序列,它们是1..88范困内的整数,每个音符代表钢琴上的一个键。不幸的是,这种旋律的表现忽略了音乐时间的概念;但是,这个编程任务是关于音符而不是时间。

许多作曲家围绕一个重复的主题构建他们的音乐,作为整个旋律的子序列,是我们表示中的一系列整数。旋律的子序列是一个主题,如果满足:

• 是至少五个音符长。

• 在音乐片段的其他地方再次出现(可能转换)。

• 与其至少一个其他出现不相交(即,不重叠)。

转换意味着将一个恒定的正值或负值添加到主题子序列中的每个音符值。

给定一个旋律,计算最长主题的长度(音符数)。

此问题的解决方案有一秒的时间限制!

二 输入和输出

1 输入

输入包含几个测试用例。每个测试用例的第一行包含整数 N。以下 n 个整数表示音符序列。

最后一个测试用例后跟一个零。

2 输出

对于每个测试用例,输出文件应包含一行,其中一个整数表示最长主题的长度。如果没有主题,则输出 0。

三 输入和输出样例

1 输入样例

30

25 27 30 34 39 45 52 60 69 79 69 60 52 45 39 34 30 26 22 18 82 78 74 70 66 67 64 60 65 80

0

2 输出样例

5

四 分析和设计

1 分析

本问题求解不重叠,长度大于等于 5 的最长重复子串的长度。可采用后缀数组及二分法求解。

2 设计

a 逐项求差,将问题转化为普通的求子串问题。

b 求解 sa 数组。

c 求解 rank 数组和 height 数组。

d 使用二分法求解,对特定的长度 mid,判断是否满足 height 大于等于 mid,且 sa 最大最小差值也大于等于mid (保证不重复)。

五 代码

package com.platform.modules.alg.alglib.poj1743;

public class Poj1743 {
    public String output = "";

    private int maxn = 20010;
    private int maxm = maxm = 200;
    int n;
    int k; // 重复 k 次

    int s[] = new int[maxn];
    int ss[] = new int[maxn];
    int sa[] = new int[maxn];
    // 名称数组
    int rank[] = new int[maxn];
    // 高度数组
    int height[] = new int[maxn];
    int wa[] = new int[maxn];
    int wb[] = new int[maxn];
    int wv[] = new int[maxn];
    // c[]用于基数排序统计
    int c[] = new int[maxm];

    boolean cmp(int r[], int a, int b, int l) {
        return (r[a] == r[b]) && (r[a + l] == r[b + l]);
    }

    // 交换两个数组的内容 https://blog.csdn.net/m0_56793367/article/details/115599564
    public static void swap(int[] array1, int[] array2) {
        int tmp = 0;
        for (int i = 0; i < array1.length; i++) {
            tmp = array1[i];
            array1[i] = array2[i];
            array2[i] = tmp;
        }
    }

    void da(int r[], int sa[], int n, int m) {
        int i, k, p;
        int x[] = wa;
        int y[] = wb;
        for (i = 0; i < m; i++) // 基数排序
            c[i] = 0;
        for (i = 0; i < n; i++)
            c[x[i] = r[i]]++;
        for (i = 1; i < m; i++)
            c[i] += c[i - 1];
        for (i = n - 1; i >= 0; i--)
            sa[--c[x[i]]] = i;
        for (k = 1; k <= n; k <<= 1) {
            // 直接利用 sa 排序第二关键字
            p = 0;
            for (i = n - k; i < n; i++)
                y[p++] = i; // 补零的位置下标排在最前面
            for (i = 0; i < n; i++)
                if (sa[i] >= k)
                    y[p++] = sa[i] - k;
            // 基数排序第一关键字
            for (i = 0; i < n; i++)
                wv[i] = x[y[i]]; // 将第二关键字排序结果转换为名次,进行排序
            for (i = 0; i < m; i++)
                c[i] = 0;
            for (i = 0; i < n; i++)
                c[wv[i]]++;
            for (i = 1; i < m; i++)
                c[i] += c[i - 1];
            for (i = n - 1; i >= 0; i--)
                sa[--c[wv[i]]] = y[i];
            // 根据 sa 和 x 数组,重新计算新的x数组
            swap(x, y); // y 数组已经没有用,更新x需要使用x本身数据,因此放入y使用
            p = 1;
            x[sa[0]] = 0;
            for (i = 1; i < n; i++)
                x[sa[i]] = cmp(y, sa[i - 1], sa[i], k) ? p - 1 : p++;
            if (p >= n)//排序结束
                break;
            m = p;
        }
    }

    void calheight(int r[], int sa[], int n) {
        int i, j, k = 0;
        for (i = 1; i <= n; i++)
            rank[sa[i]] = i;
        for (i = 0; i < n; i++) {
            if (k > 0)
                k--;
            j = sa[rank[i] - 1];
            while (r[i + k] == r[j + k])
                k++;
            height[rank[i]] = k;
        }
    }

    boolean check(int mid) {
        int mx = sa[1], mn = sa[1];
        for (int i = 2; i <= n; i++) {
            if (height[i] >= mid) {
                mx = Math.max(mx, sa[i]);
                mn = Math.min(mn, sa[i]);
                if (mx - mn >= mid)
                    return true;
            } else {
                mx = sa[i];
                mn = sa[i];
            }
        }
        return false;
    }

    void solve() {
        int L = 4, R = n, res = -1; // 答案必须>=4
        while (L <= R) {
            int mid = (L + R) >> 1;
            if (check(mid)) {
                res = mid;
                L = mid + 1;
            } else
                R = mid - 1;
        }
        if (res < 4)
            output += "0\n";
        else
            output += res + 1 + "\n";
    }
    
    public String cal(String input) {
        String[] line = input.split("\n");
        n = Integer.parseInt(line[0]);
        String[] num = line[1].split(" ");


        for (int i = 0; i < n; i++)
            s[i] = Integer.parseInt(num[i]);
        if (n < 9) {
            output += "0\n";
        }
        n--;
        for (int i = 0; i < n; i++)
            ss[i] = this.s[i + 1] - this.s[i] + 100;
        ss[n] = 0;
        da(ss, sa, n + 1, 200);
        calheight(ss, sa, n);
        solve();
        return output;
    }
}

六 测试

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值