// Original C++ solution (slower):
#include <bits/stdc++.h>

#define LLI long long int

#define FOR(v, a, b) for(LLI v = (a); v < (b); ++v)

#define FORE(v, a, b) for(LLI v = (a); v <= (b); ++v)

#define REP(v, n) FOR(v, 0, n)

#define REPE(v, n) FORE(v, 0, n)

#define REV(v, a, b) for(LLI v = (a); v >= (b); --v)

#define ALL(x) (x).begin(), (x).end()

#define RALL(x) (x).rbegin(), (x).rend()

#define ITR(it, c) for(auto it = (c).begin(); it != (c).end(); ++it)

#define RITR(it, c) for(auto it = (c).rbegin(); it != (c).rend(); ++it)

#define EXIST(c,x) ((c).find(x) != (c).end())

#define fst first

#define snd second

#define popcount __builtin_popcount

#define UNIQ(v) (v).erase(unique(ALL(v)), (v).end())

#define bit(i) (1LL<<(i))



#ifdef DEBUG

#include <misc/C++/Debug.cpp>

#else

#define dump(...) ((void)0)

#endif



#define gcd __gcd



using namespace std;

template <class T> constexpr T lcm(T m, T n){return m/gcd(m,n)*n;}



template <typename I> void join(ostream &ost, I s, I t, string d=" "){for(auto i=s; i!=t; ++i){if(i!=s)ost<<d; ost<<*i;}ost<<endl;}

template <typename T> istream& operator>>(istream &is, vector<T> &v){for(auto &a : v) is >> a; return is;}



template <typename T, typename U> bool chmin(T &a, const U &b){return (a>b ? a=b, true : false);}

template <typename T, typename U> bool chmax(T &a, const U &b){return (a<b ? a=b, true : false);}

template <typename T, size_t N, typename U> void fill_array(T (&a)[N], const U &v){fill((U*)a, (U*)(a+N), v);}



struct Init{

  Init(){

    cin.tie(0);

    ios::sync_with_stdio(false);

  }

}init;



template <typename T, LLI BASE, LLI MOD> class RollingHash{

private:

  static vector<LLI> pow;



public:

  static void init(int N){

    pow.assign(N+1,0);

    pow[0] = 1;

    FORE(i,1,N){

      pow[i] = pow[i-1]*BASE % MOD;

    }

  }



  static vector<LLI> generate_hash(const T &s){

    vector<LLI> ret(s.size()+1);



    FORE(i,1,(int)s.size()){

      ret[i] = (ret[i-1]*BASE+s[i-1]) % MOD;

    }

    

    return ret;

  }



  static LLI hash(const vector<LLI> &shash, int i, int j){ // [i, j)

    return (shash[j] - shash[i] * pow[j-i] + MOD*MOD) % MOD;

  }



  static LLI hash(const T &p){

    LLI phash = 0;

    REP(i,(int)p.size()) phash = (phash*BASE+p[i]) % MOD;

    return phash;

  }



  

  static vector<int> find(const vector<LLI> &shash, const T &p){

    vector<int> ret;

    int pl = p.size();

    LLI phash = hash(p);

    REP(i,(int)shash.size()-pl) if(hash(shash,i,i+pl) == phash) ret.push_back(i);

    return ret;

  }

};



template <typename T, LLI BASE, LLI MOD> vector<LLI> RollingHash<T,BASE,MOD>::pow;



using R1 = RollingHash<string,31,1000000009>;

using R2 = RollingHash<string,41,998244353>;



int main(){

  R1::init(10000);

  R2::init(10000);

  

  int N;

  string s;

  while(cin >> N >> s){



    auto hs1 = R1::generate_hash(s);

    auto hs2 = R2::generate_hash(s);

    

    int ans = 0;



    auto check =

      [&](int L){

        set<pair<LLI,LLI>> store;

        queue<pair<LLI,LLI>> q;



        REP(i,N-L+1){

          auto h1 = R1::hash(hs1,i,i+L);

          auto h2 = R2::hash(hs2,i,i+L);

        

          pair<LLI,LLI> H(h1,h2);



          if(EXIST(store,H)){

            return true;

          }



          if((int)q.size() == L-1){

            q.push(H);

            auto t = q.front(); q.pop();

            store.insert(t);

          }else{

            q.push(H);

          }

        }



        return false;

      };



    int lb = 0, ub = N, mid;

    while(abs(lb-ub) > 1){

      mid = (lb+ub)/2;



      if(check(mid)){

        lb = mid;

      }else{

        ub = mid;

      }

    }



    ans = lb;



    cout << ans << endl;

  }





  return 0;

}


// Optimized solution to the same problem (faster):
#include <bits/stdc++.h>





template <typename T>

struct PrintVector{

  const std::vector<T> value;

  const std::string open, close, delim;



  PrintVector(const std::vector<T> &value, const std::string &delim, const std::string &open, const std::string &close):

    value(value), open(open), close(close), delim(delim){}



  std::ostream& operator()(std::ostream &s) const {

    s << open;

    

    const auto cbegin = value.cbegin();

    const auto cend = value.cend();



    for(auto it = cbegin; it != cend; ++it){

      if(it != cbegin) s << delim;

      s << *it;

    }



    s << close;

    return s;

  }



  friend std::ostream& operator<<(std::ostream &s, const PrintVector<T> &manip){

    return manip(s);

  }

};



template <typename T>

auto print_vector(const std::vector<T> &value, const std::string delim, const std::string &open = "", const std::string &close = ""){

  return PrintVector<T>(value, delim, open, close);

}







template <typename T> class SuffixArray{

public:

  const T str;

  const int n;

  std::vector<int> suffix_array;

  

  SuffixArray(const T &s): str(s), n(s.size()), suffix_array(n){

    std::vector<int> temp(n);

    for(int i = 0; i < n; ++i) temp[i] = s[i];



    for(int l = 1; l < n; l *= 2){

      std::map<std::pair<int,int>, int> m2;

      for(int i = 0; i < n; ++i){

        m2[std::make_pair(temp[i], i+l >= n ? -1 : temp[i+l])] = 0;

      }



      int i=0;



      for(auto it = m2.begin(); it != m2.end(); ++it) it->second = i++;



      for(int i = 0; i < n; ++i) temp[i] = m2[std::make_pair(temp[i], i+l >= n ? -1 : temp[i+l])];

    }

    

    for(int i = 0; i < n; ++i) suffix_array[temp[i]] = i;

  }



  int operator[](int i) const {return suffix_array[i];}



  bool starts_with(const T &s, int k) const {

    if(s.size() <= str.size() - k){

      for(int i = 0; i < (int)s.size(); ++i){

        if(s[i] != str[k+i]) return false;

      }

      return true;

    }

    return false;

  }



  int lower_bound(const T &s) const {

    auto check =

      [&](int x){

        for(int i = 0; i < (int)s.size(); ++i){

          if(suffix_array[x]+i >= (int)str.size()) return false;

          if(s[i] < str[suffix_array[x]+i]) return true;

          if(s[i] > str[suffix_array[x]+i]) return false;

        }

        return true;

      };



    int lb = -1, ub = n;

    while(abs(lb-ub)>1){

      int mid = (lb+ub)/2;



      if(check(mid)){

        ub = mid;

      }else{

        lb = mid;

      }

    }

    

    return ub;

  }



  int upper_bound(const T &s) const {

    T t(s);



    t.back()++;

    int ret = lower_bound(t);

    t.back()--;

    return ret;

  }

};







template <typename T>

struct LCPArray{

  const int n;

  std::vector<int> rank, lcp_array;



  LCPArray(const SuffixArray<T> &sa): n(sa.n), rank(n), lcp_array(n-1){

    for(int i = 0; i < n; ++i) rank[sa[i]] = i;



    int h = 0;

    for(int i = 0; i < n; ++i){

      if(rank[i] == 0) continue;

      int j = sa[rank[i]-1];



      if(h) --h;

      while(j+h < n and i+h < n){

        if(sa.str[j+h] != sa.str[i+h]) break;

        ++h;

      }



      lcp_array[rank[i]-1] = h;

    }

  }

};





template <typename Semigroup>

class SparseTable{

  using value_type = typename Semigroup::value_type;

  

  std::vector<std::vector<value_type>> a;

  std::vector<int> log_table;

  

public:

  SparseTable(const std::vector<value_type> &v){

    int n = v.size();

    int logn = 0;

    while((1 << logn) <= n) ++logn;

    

    a.assign(n, std::vector<value_type>(logn));

    for(int i = 0; i < n; ++i) a[i][0] = v[i];

    for(int j = 1; j < logn; ++j){

      for(int i = 0; i < n; ++i){

        a[i][j] = Semigroup::op(a[i][j-1], a[std::min<int>(n-1, i+(1<<(j-1)))][j-1]);

      }

    }



    log_table.assign(n+1, 0);

    for(int i = 2; i < n+1; ++i) log_table[i] = log_table[i>>1] + 1;

  }

  

  inline value_type get(int s, int t) const { // [s,t)

    int k = log_table[t-s];

    return Semigroup::op(a[s][k], a[t-(1<<k)][k]);

  }



  inline value_type get(std::vector<std::pair<int,int>> st) const {

    value_type ret;

    bool t = true;



    for(const auto &p : st){

      if(p.first < p.second){

        if(t){

          ret = get(p.first, p.second);

          t = false;

        }else{

          ret = Semigroup::op(ret, get(p.first, p.second));

        }

      }

    }



    return ret;

  }

};





template <typename T>

struct MinMonoid{

  using value_type = T;

  constexpr inline static value_type id(){return std::numeric_limits<T>::max();}

  constexpr inline static value_type op(const value_type &a, const value_type &b){return std::min(a, b);}

};









int main(){

  int N; std::cin >> N;

  std::string S; std::cin >> S;



  int ans = 0;



  SuffixArray<std::string> sa(S);

  LCPArray<std::string> lcp(sa);



  SparseTable<MinMonoid<int>> sp(lcp.lcp_array);



  for(int i = 0; i < N; ++i){

    for(int j = 0; j < i; ++j){

      int l = lcp.rank[i], r = lcp.rank[j];

      if(l > r) std::swap(l, r);

      int t = sp.get(l, r);

      if(j + t <= i) ans = std::max(ans, t);

    }

  }



  std::cout << ans << std::endl;



  return 0;

}


// Fastest known solution for this problem: